A contact form that fails honestly
2 min readNext.js · API Routes · Resend0 views
Most contact forms on portfolio sites have the same dirty secret: they paint a green checkmark and say “sent” while no email goes anywhere. This site's form is built around one rule — the UI is never allowed to claim something the backend didn't do. If delivery isn't configured, the form says so and offers a direct mailto: link. Honest failure is a feature.
The contract
- Server-side validation only — the client can be bypassed, so the route re-checks everything: required fields, an email pattern, and hard length caps (name 120, email 200, subject 160, message 5000).
- A honeypot field named
company— invisible to humans, irresistible to bots. Filled honeypots are silently accepted ({ ok: true }) but nothing is sent, so bots learn nothing. - Delivery only happens when the environment says it can: no
RESEND_API_KEYorCONTACT_TO_EMAILmeans a typednot_configuredresponse with a 503 — not a fake success. - The browser sees typed error codes, never raw provider output. Logs stay server-side.
The honeypot, in ten lines
const company = clean(body.company); // hidden from humans
if (company) {
// Bot filled the trap — pretend success, send nothing.
return NextResponse.json({ ok: true });
}I prefer a honeypot to a CAPTCHA for this context: it costs zero user friction, needs no third-party script, and is enough to filter the drive-by spam that actually hits a small personal site. It is not a security boundary — it is a noise filter, and it knows it.
Delivery behind env vars, no SDK
Delivery goes through Resend with a plain fetch — no SDK dependency, no abstraction layer to rot. The API key never touches the browser; RESEND_FROM and CONTACT_TO_EMAIL come from the environment. If Resend itself rejects the send, the route logs the status and returns delivery_failed with a 502. The user is told it failed; the log tells me why.
What 'honest' buys you
- Trust — a visitor who is told the truth uses the fallback instead of shouting into the void.
- Debuggability — every failure mode has a distinct code:
invalid_json,missing_fields,invalid_email,not_configured,delivery_failed. - Zero lock-in — swapping Resend for another provider means rewriting one
fetchblock, not untangling an SDK.
Build contact forms like you will be the one answering at 2am: nothing pretends, nothing disappears quietly, and every failure has a name.

Written by
Shihab Hossain — Full-Stack Developer
I build thoughtful digital products — from fast web experiences to mobile apps and the infrastructure behind them.
Work with me