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.