Skip to content

A contact form that fails honestly

2 min readNext.js · API Routes · Resend0 views

nameemailmessagehoneypot — bots only

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_KEY or CONTACT_TO_EMAIL means a typed not_configured response 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 fetch block, 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.

Shihab Hossain — full-stack developer portrait

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

All notes