DS DevShelfHub Projects · AI tools

AI Email Reply Generator: Tone-Aware Drafts With Node.js and Groq

Beginner

By DevShelfHub

Paste an email, pick a tone (Formal, Friendly, Short), and get a Groq-powered draft reply—Express API plus vanilla HTML/CSS/JS, no frontend build.

Node.js Express JavaScript Groq API

View on GitHub

AI Email Reply Generator — Express + Groq tone-aware email drafts

This is a small full-stack demo with no frontend build step: a static page (HTML, CSS, and vanilla JavaScript) talks to a Node.js + Express server. You paste an email, pick a tone (Formal, Friendly, or Short), and the server calls the Groq chat API (OpenAI-compatible) to draft a reply you can copy into your mail client.

Purpose: practice wiring a real HTTP API, keeping secrets on the server, and returning JSON the browser can render—without React, Vite, or Streamlit in the loop.

Typical use: draft a first-pass reply to routine messages, then edit tone or facts before you send anything.

Key features:

  • Express serves the client/ folder and exposes POST /generate-reply.
  • Groq API key lives only in server/.env (never shipped to the browser).
  • Validation on empty input, tone whitelist, and an 8000-character cap on both client and server.
  • Copy-to-clipboard for the generated reply; clear status and error messages on failure.

Privacy note: pasted email leaves your machine for the model provider. Do not use confidential content unless your policy allows it.

Overall flow

User pastes email + picks tone
      ↓
Browser POST /generate-reply { email, tone }
      ↓
Express validates input
      ↓
Server calls Groq chat/completions (Bearer key from .env)
      ↓
JSON { reply } returned
      ↓
UI shows reply; user can copy

Step-by-step implementation

  1. Set up the project
    • Install Node.js 18 or newer
    • Create server/ and client/ folders
    • In server/, run npm init and add dependencies
  2. Install server dependencies
    • express for HTTP and static files
    • dotenv to load GROQ_API_KEY
    • cors if you later split frontend origin from API (optional for same-origin)
  3. Build the static UI
    • index.html: textarea, tone <select>, generate button, status line, reply output, copy button
    • styles.css: layout and readable typography
    • app.js: fetch to /generate-reply
  4. Secure the API key
    • Copy .env.example to .env under server/
    • Set GROQ_API_KEY; optionally GROQ_MODEL and PORT
    • Keep .env out of version control
  5. Implement the API route
    • Parse JSON body, validate email string and tone
    • Map tone to a natural-language instruction for the model
    • fetch Groq’s OpenAI-compatible endpoint with system + user messages
    • Return { "reply": "..." } or { "error": "..." } with appropriate HTTP status
  6. Wire the client
    • Disable duplicate submits while a request is in flight
    • Surface server error messages in the status line
    • Enable copy only when a non-empty reply exists

Code implementation

The repo splits responsibilities: Express owns secrets and the Groq call; the browser only sends the email text and tone.

Server (server/index.js)

javascript

/**
 * AI Email Reply Generator — Express server
 *
 * Serves the static frontend from ../client and exposes POST /generate-reply
 * which forwards requests to the Groq API (OpenAI-compatible chat format).
 */

require("dotenv").config({ path: require("path").join(__dirname, ".env") });

const path = require("path");
const express = require("express");
const cors = require("cors");

const app = express();
const PORT = process.env.PORT || 3000;

// Groq uses the OpenAI-compatible Chat Completions API
const GROQ_API_URL = "https://api.groq.com/openai/v1/chat/completions";
// Fast, capable model; change via GROQ_MODEL in .env if you prefer another
const DEFAULT_MODEL = "llama-3.3-70b-versatile";

/** Maximum characters accepted for the pasted email (matches client limit) */
const MAX_EMAIL_LENGTH = 8000;

app.use(cors());
app.use(express.json({ limit: "256kb" }));

// Serve HTML, CSS, and JS from the client folder
app.use(express.static(path.join(__dirname, "..", "client")));

/**
 * POST /generate-reply
 * Body: { "email": string, "tone": "formal" | "friendly" | "short" }
 * Returns: { "reply": string } or { "error": string }
 */
app.post("/generate-reply", async (req, res) => {
  const apiKey = process.env.GROQ_API_KEY;
  if (!apiKey) {
    return res.status(500).json({
      error:
        "Server is missing GROQ_API_KEY. Add it to server/.env and restart.",
    });
  }

  const { email, tone } = req.body || {};

  if (typeof email !== "string" || !email.trim()) {
    return res.status(400).json({
      error: "Please paste the email you want to reply to.",
    });
  }

  const trimmed = email.trim();
  if (trimmed.length > MAX_EMAIL_LENGTH) {
    return res.status(400).json({
      error: `Email is too long. Please use at most ${MAX_EMAIL_LENGTH} characters.`,
    });
  }

  const allowedTones = ["formal", "friendly", "short"];
  const normalizedTone =
    typeof tone === "string" ? tone.toLowerCase().trim() : "";
  if (!allowedTones.includes(normalizedTone)) {
    return res.status(400).json({
      error: 'Tone must be one of: "formal", "friendly", or "short".',
    });
  }

  // Human-readable tone label for the model
  const toneLabel =
    normalizedTone === "formal"
      ? "formal and professional"
      : normalizedTone === "friendly"
        ? "warm and friendly"
        : "brief and to the point (keep the reply short)";

  const userPrompt = `Write a ${toneLabel} reply to the following email:\n\n${trimmed}`;

  const model = process.env.GROQ_MODEL || DEFAULT_MODEL;

  try {
    const groqResponse = await fetch(GROQ_API_URL, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${apiKey}`,
      },
      body: JSON.stringify({
        model,
        messages: [
          {
            role: "system",
            content:
              "You are a helpful assistant that writes clear, natural email replies. Output only the reply text the user can send—no subject line unless the user explicitly asked for one, and no explanations or quotes.",
          },
          { role: "user", content: userPrompt },
        ],
        temperature: 0.7,
      }),
    });

    const data = await groqResponse.json().catch(() => ({}));

    if (!groqResponse.ok) {
      const message =
        data?.error?.message ||
        `Groq API error (${groqResponse.status}). Try again in a moment.`;
      return res.status(502).json({ error: message });
    }

    const reply =
      data?.choices?.[0]?.message?.content?.trim() || "";

    if (!reply) {
      return res.status(502).json({
        error: "The AI returned an empty reply. Please try again.",
      });
    }

    return res.json({ reply });
  } catch (err) {
    console.error("generate-reply error:", err);
    return res.status(502).json({
      error:
        "Could not reach the AI service. Check your internet connection and try again.",
    });
  }
});

app.listen(PORT, () => {
  console.log(`Server running at http://localhost:${PORT}`);
  console.log(`Open that URL in your browser to use the app.`);
});

Client (client/app.js)

javascript

/**
 * AI Email Reply Generator — frontend logic (vanilla JS)
 *
 * Sends the email text and tone to POST /generate-reply on the same origin
 * (when you open the app via the Express server).
 */

const MAX_CHARS = 8000;

const emailInput = document.getElementById("email-input");
const toneSelect = document.getElementById("tone-select");
const generateBtn = document.getElementById("generate-btn");
const statusMessage = document.getElementById("status-message");
const replyOutput = document.getElementById("reply-output");
const copyBtn = document.getElementById("copy-btn");
const charCount = document.getElementById("char-count");

function setStatus(text, isError = false) {
  statusMessage.textContent = text || "";
  statusMessage.classList.toggle("error", isError);
}

function updateCharCount() {
  const len = emailInput.value.length;
  charCount.textContent = `${len} / ${MAX_CHARS}`;
}

emailInput.addEventListener("input", updateCharCount);
updateCharCount();

generateBtn.addEventListener("click", async () => {
  const email = emailInput.value.trim();
  const tone = toneSelect.value;

  if (!email) {
    setStatus("Please paste the email you want to reply to.", true);
    replyOutput.textContent = "";
    copyBtn.disabled = true;
    return;
  }

  setStatus("Generating reply…");
  replyOutput.textContent = "";
  copyBtn.disabled = true;
  generateBtn.disabled = true;

  try {
    const res = await fetch("/generate-reply", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ email, tone }),
    });

    const data = await res.json().catch(() => ({}));

    if (!res.ok) {
      const msg =
        data.error ||
        `Something went wrong (${res.status}). Please try again.`;
      setStatus(msg, true);
      return;
    }

    if (!data.reply) {
      setStatus("No reply was returned. Please try again.", true);
      return;
    }

    replyOutput.textContent = data.reply;
    copyBtn.disabled = false;
    setStatus("Done! Review and edit before sending.");
  } catch (err) {
    console.error(err);
    setStatus(
      "Could not reach the server. Make sure it is running and refresh the page.",
      true
    );
  } finally {
    generateBtn.disabled = false;
  }
});

copyBtn.addEventListener("click", async () => {
  const text = replyOutput.textContent;
  if (!text) return;

  try {
    await navigator.clipboard.writeText(text);
    setStatus("Copied to clipboard.");
  } catch {
    setStatus("Copy failed. You can select the text and copy manually.", true);
  }
});

Complete code Project link (GitHub)


Demo

Screen recording of the app: paste an email, pick a tone (Formal, Friendly, or Short), generate a reply, and copy it to the clipboard.


Run it locally

  • From server/, run npm install.
  • Copy .env.example to .env (same folder) and set GROQ_API_KEY.
  • Start the app with npm start, then open http://localhost:3000 (or whatever PORT you set in .env).
  • For auto-restart while editing, use npm run dev ( node --watch).

How the code works (step-by-step)


1. Static hosting + JSON body

  • express.static serves client/ so the UI loads from the same origin as the API
  • express.json({ limit: "256kb" }) parses JSON bodies safely under a size cap

2. Validation before any external call

  • Missing or blank email → 400 with a clear message
  • Length over 8000 characters → 400 (matches maxlength in the HTML)
  • tone normalized to lowercase and checked against formal, friendly, short

3. Prompting Groq

  • A system message constrains output to sendable reply text only (no meta commentary)
  • A user message combines the tone phrase with the pasted email
  • Model name comes from GROQ_MODEL or defaults to llama-3.3-70b-versatile

4. Client UX details

  • Character counter updates on every input event
  • finally re-enables the generate button even when the request errors
  • Clipboard API with a fallback message if the browser blocks copy without permission

Tips & Production Considerations

Never expose the API key to the browser

The Express server acts as a proxy so the Groq key stays in .env. If you move to a serverless function (Vercel, Netlify), store the key in the platform's secrets panel and keep the same pattern: the browser sends email text, the function calls Groq, the reply comes back. Never embed the key in client-side JavaScript.

Add rate limiting before sharing publicly

Without rate limiting, anyone who discovers the endpoint can burn through your Groq quota. A lightweight middleware like express-rate-limit (e.g. 10 requests per minute per IP) is enough for a demo. For tighter control, add a simple API key or session cookie check.

Extend with custom tones without touching the model

Add new tones by writing a one-sentence instruction string (e.g. "Reply apologetically, acknowledge the inconvenience, and offer a concrete next step"). Update the tone whitelist in the server, add the option to the HTML <select>, and you are done. The same model prompt structure works for any communication style.

Sanitize pasted email content

The 8 000-character cap prevents oversized payloads, but pasted HTML from some mail clients can include tracking pixels or base64 images. If you extend the app to accept rich text, strip HTML tags server-side before sending to the model to keep token counts low and avoid prompt-injection attempts.

Deploy with one command

Because the frontend is static and the backend is a single Express file, you can deploy to Render, Railway, or any platform that supports Node.js. Set GROQ_API_KEY as an environment variable and the app is live.


AI Email Reply Generator FAQ

What is the AI Email Reply Generator?

The AI Email Reply Generator is a beginner-friendly Node.js project that drafts email replies in the tone you pick. You paste the incoming email, choose Formal, Friendly, or Short, and a Groq-powered Express endpoint returns a ready-to-edit reply.

Is the AI Email Reply Generator free to use?

The source code is free and open on GitHub. You only pay for Groq API usage, which is fast and inexpensive—a single drafted reply costs a fraction of a cent on Groq's free tier.

What tech stack does the AI Email Reply Generator use?

Node.js with Express on the backend, the Groq REST API for chat completions, and vanilla HTML, CSS, and JavaScript on the frontend. There is no React, no Tailwind build, and no bundler—just an index.html that calls the Express endpoint.

How does tone control work in the AI Email Reply Generator?

The selected tone is passed to the server, which builds a system prompt with explicit style instructions (e.g., warm but brief for Friendly, formal salutation and sign-off for Formal, two sentences max for Short). The same incoming email plus a different tone yields a different draft.

Can I customize the tones in the AI Email Reply Generator?

Yes. Tones are just strings in the server's system prompt template—add Apologetic, Confident, or Bilingual by editing the prompt and adding a button in index.html. No model fine-tuning is needed.

What are alternatives to the AI Email Reply Generator?

For longer-form copywriting, see the AI Product Description Generator. For structured response with judgment and scoring, see the AI Debate Partner. For meeting follow-ups instead of email replies, see the Meeting & Lecture Summarizer.

Browse all →