6 min read

Keeping Contact Form Email Server-Side in a Next.js Portfolio

How I moved portfolio contact delivery behind a Next.js Route Handler with Resend, server-only secrets, input handling, and a separate acknowledgement path.

Next.jsResendAPISecurityServerless

A portfolio contact form looks small, but it crosses several trust boundaries: an untrusted browser submits user input, a server needs to call an email provider, and the visitor needs a useful success or failure response. The implementation becomes especially important when the email provider requires a secret API credential.

I moved the portfolio's contact delivery behind a Next.js Route Handler instead of making the browser responsible for calling the email service. The resulting endpoint is deliberately narrow: validate the request, build safe email content, call the provider from the server, and return a small JSON result to the form.

The browser should not own the provider credential

The most important boundary is where the Resend API key lives. The contact endpoint reads `RESEND_API_KEY` and `RESEND_FROM` from server-side environment variables and sends the provider request from the route handler. The browser only knows about the portfolio endpoint; it never needs the Resend credential.

This is a useful pattern for small serverless applications because it avoids adding a separate backend service just to protect one secret. Next.js Route Handlers provide a server-side HTTP boundary, while Vercel can supply the environment variables to the deployed function.

  • Keep the provider API key in the deployment environment, not in client-side JavaScript.
  • Expose only the application endpoint to the contact form.
  • Fail with a server error when required email configuration is missing rather than attempting a client-side fallback.
  • Treat the provider call as an integration behind the application boundary.

Validate before building an email

The endpoint parses the JSON body and normalizes the four fields the form expects: name, email, subject, and message. Empty values are rejected before any provider request is made. That check is intentionally simple, but it establishes an important rule: the email service is not the validation layer for the application's own request contract.

There is another subtle boundary when generating HTML. User-controlled strings are escaped before they are inserted into the HTML email template. This is not about making the email provider validate the message; it is about ensuring that markup supplied through a contact form is treated as text inside the generated document.

  • Parse and type-check the incoming fields.
  • Trim the values before checking required fields.
  • Reject incomplete requests with HTTP 400.
  • Escape user-controlled values before interpolating them into HTML.
  • Keep plain-text alternatives alongside HTML for the outgoing messages.

One submission produces two different email jobs

The endpoint sends a notification to the portfolio owner and then attempts a separate acknowledgement to the visitor. These messages have different responsibilities, so keeping them as separate provider calls makes the failure behavior explicit.

The notification is the critical path: if Resend rejects it, the endpoint returns a 502-style integration failure instead of claiming success. The acknowledgement is best-effort. If that second request fails, the server logs the failure but still returns success because the portfolio owner has already received the enquiry.

  • Owner notification failure means the submission is not reported as successful.
  • The visitor acknowledgement is attempted only after the owner notification succeeds.
  • A failed acknowledgement does not erase a successfully delivered owner notification.
  • The visitor's address is used as the reply-to address on the owner notification.

The API boundary is also a failure boundary

The endpoint distinguishes configuration errors, invalid input, provider failures, and unexpected exceptions. That makes the client response more useful without exposing provider credentials or raw upstream error details. The server logs the provider response when a send fails, while the browser receives a stable application-level message.

This is a small example of a broader serverless design rule: an external service should not define your public API contract. The portfolio endpoint owns its response shape and can change email providers later without forcing the form UI to understand provider-specific status codes or payloads.

What I would harden next

The current endpoint is intentionally small, but an internet-facing contact form should not be treated as complete just because the email arrives. A production-hardening pass would add rate limiting or abuse controls, stronger email-address validation, request-size limits, and an idempotency strategy so retries cannot create accidental duplicate notifications.

Resend now documents idempotency keys for its email API, specifically to protect against duplicate sends caused by retries, repeated submissions, and network failures. That makes idempotency a natural next improvement if the portfolio starts receiving meaningful traffic or if the client gains retry behavior.

  • Add rate limiting or another abuse-control mechanism.
  • Validate email addresses more deliberately than a non-empty string.
  • Cap request body size before processing large messages.
  • Introduce an idempotency key for the owner notification when duplicate delivery becomes a realistic concern.
  • Add structured logging or request identifiers for easier troubleshooting.

The broader lesson

A contact form is a useful miniature of full-stack engineering. The UI is only one piece; the real design lives in the boundaries between untrusted input, server-side secrets, external APIs, and user-visible failure states.

For a small portfolio, the best architecture is not necessarily a separate backend. A focused Next.js Route Handler can provide the server-side boundary, while the email provider handles delivery. The important part is keeping responsibilities explicit: the browser submits intent, the server owns the credential and API contract, and the provider handles email delivery.