Guides/ Node.js
Guides

Node.js

An Express handler that classifies, then delivers or quarantines. The shape is the same in Next.js, Hono, Fastify or SvelteKit.

Works with any Node 18+ runtime (native fetch). Swap Express for Next.js route handlers, Hono, Fastify or SvelteKit actions; only the request plumbing changes.

The handler

import express from "express";

const app = express();
app.use(express.urlencoded({ extended: false }));
app.set("trust proxy", true); // so req.ip is the visitor behind your proxy, not the proxy

const BOT = new Set(["HONEYPOT_FILLED", "FAST_SUBMIT", "URL_IN_NAME", "DUP_KNOWN_SPAM",
  "IP_REPUTATION_BAD", "EMAIL_REPUTATION_BAD", "EMAIL_DOMAIN_REPUTATION_BAD"]);

async function classify(fields, req) {
  try {
    const r = await fetch("https://api.spamex.dev/v1/classify", {
      method: "POST",
      headers: { Authorization: `Bearer ${process.env.SPAMEX_KEY}`, "Content-Type": "application/json" },
      body: JSON.stringify({
        name: fields.name, email: fields.email, subject: fields.subject, message: fields.message,
        honeypot: fields.website,                          // your hidden trap field
        form_load_ts: Number(fields.form_load_ts) || undefined,
        submit_ts: Math.floor(Date.now() / 1000),
        ip: req.ip, user_agent: req.get("user-agent"),
      }),
      signal: AbortSignal.timeout(5000),
    });
    return r.ok ? await r.json() : null;                 // null = fail open
  } catch {
    return null;                                         // network error or timeout: fail open
  }
}

app.post("/contact", async (req, res) => {
  const v = await classify(req.body, req);
  if (v?.label === "spam") {
    if (v.reasons.some((c) => BOT.has(c))) return res.status(200).send("Thanks!"); // drop silently
    await quarantine(req.body, v);                       // review folder; keep v.id for feedback
    return res.status(200).send("Thanks! If you do not hear back, email [email protected].");
  }
  await deliver(req.body, v);                            // send the email; store v?.id next to it
  res.status(200).send("Thanks!");
});

Feedback from your inbox

If your mail client or admin has a “not spam” action that reaches your backend, wire it to feedback with the stored id:

await fetch("https://api.spamex.dev/v1/feedback", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.SPAMEX_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({ id: storedVerdictId, label: "ham" }),
});

Next.js route handler

// app/api/contact/route.ts
export async function POST(req: Request) {
  const fields = Object.fromEntries(await req.formData());
  const ip = req.headers.get("x-forwarded-for")?.split(",")[0].trim();
  const r = await fetch("https://api.spamex.dev/v1/classify", {
    method: "POST",
    headers: { Authorization: `Bearer ${process.env.SPAMEX_KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify({ ...fields, honeypot: fields.website, ip, user_agent: req.headers.get("user-agent") }),
    signal: AbortSignal.timeout(5000),
  }).catch(() => null);
  const verdict = r?.ok ? await r.json() : null;
  // deliver / quarantine as above
  return Response.json({ ok: true });
}

Fail open or closed?

Fail open (deliver when the API is unreachable) for contact forms: a lost customer message costs more than one spam email. Fail closed only for forms that trigger an expensive action.

↑↓ navigate↵ openDocs and blog