Using Resend for Contact Forms in Next.js
September 24, 2026 Avishka Devinda
September 24, 2026 Avishka Devinda
A contact form is one of those features that looks trivial until it reaches production.
The UI is easy.
The harder parts are:
For my Next.js projects, I like using Resend for transactional email.
The browser should never receive the Resend API key.
I send the form to a server-side endpoint:
POST /api/send/mail/talk-me
The route reads the environment variables and calls Resend from the server.
A simplified version looks like:
import { Resend } from 'resend';
export const runtime = 'nodejs';
export async function POST(request: Request) {
const body = await request.json();
const resend = new Resend(process.env.RESEND_API_KEY);
const result = await resend.emails.send({
from: process.env.RESEND_FROM_MAIL!,
to: process.env.RESEND_TO_MAIL!,
subject: `New message from ${body.name}`,
text: body.message,
});
return Response.json(result);
}
For email routes, I care more about compatibility and reliability than trying to run the function in a specialized runtime.
The route already depends on server-side packages and an external email API.
I would rather give those dependencies the normal Node.js environment.
Email delivery itself is a network request, so moving the function a few milliseconds closer to the browser is usually not the important performance problem.
I validate contact form input before calling the email provider.
For example:
const contactSchema = z.object({
name: z.string().min(2).max(80),
email: z.string().email(),
message: z.string().min(10).max(3000),
});
Then:
const parsed = contactSchema.safeParse(body);
if (!parsed.success) {
return Response.json(
{ error: 'Invalid form data' },
{ status: 400 }
);
}
This protects the endpoint from obviously invalid payloads.
Plain text email is enough for some projects.
When I want a styled email, I prefer keeping the template as a React component rather than building a long HTML string.
That makes the template easier to edit and reuse.
For example:
<TalkMeTemplate
name={name}
email={email}
message={message}
/>
The API route stays focused on validation and delivery.
The template stays focused on presentation.
I do not want every failure to become the same generic "Something went wrong."
The server should distinguish between:
The client does not need every internal detail, but it should know whether the user can retry.
A public contact endpoint is a public API.
Bots can find it.
Even a simple rate limit is better than leaving the endpoint unlimited.
I normally rate limit using an identifier such as an IP-derived key or another request-level identifier, depending on the deployment environment.
I also keep message length limited so one request cannot send an enormous payload.
A common mistake is using the visitor's email address directly as the sending address.
I prefer sending from a domain/address configured for the application and including the visitor's email in the message or reply-to field.
That keeps email authentication predictable.
After clicking Send, the button should enter a loading state.
If the request fails, the form should show an error.
If it succeeds, then I show the success state.
I do not clear the form before I know the server accepted the message.
Small details like this make the contact page feel much more reliable.
A contact form does not need a complicated backend.
My preferred setup is:
React Hook Form
↓
Zod validation
↓
Next.js Node.js Route Handler
↓
Resend
↓
Email inbox
It is small, easy to debug, and enough for most portfolio and SaaS contact forms.