Next.jsResendEmailAPI Routes

Sending Emails with Resend and Next.js: A Simple, Practical Integration

How to wire up a contact form in Next.js using Resend — covering the server-side boundary, validation, error handling, and deployment.

10 min read

Sending an email from a web application sounds simple. A user fills out a form, clicks Send, and an email arrives. But behind that button: Frontend Form → Next.js Server → Resend API → Email Provider → Recipient's Inbox.

I recently needed to add email functionality to a Next.js application and used Resend to handle the actual delivery. What stood out was how little infrastructure was required — my Next.js application handled the application logic, and Resend handled delivery.

What Is Resend?

Resend is an email API designed for developers. Instead of managing an SMTP server yourself, your application can make an API request telling Resend who the email is from, who should receive it, the subject, and the content. Your application handles the business logic. Resend handles the email delivery infrastructure.

Why Not Send From the Browser?

Your Resend API key is a secret. Anything shipped to the browser can potentially be inspected by the user. The API key stays on the server.

This is one of the reasons Next.js makes this workflow convenient: you can keep the email-sending logic inside a server-side environment. The browser never needs to know the API key exists.

Installing Resend

npm install resend
# .env.local
RESEND_API_KEY=re_xxxxxxxxxxxxxxxxx
import { Resend } from "resend";

const resend = new Resend(process.env.RESEND_API_KEY);

Creating the API Route

In a Next.js App Router application, create:app/api/send-email/route.ts. The route becomes POST /api/send-email, and our frontend sends form data there.

// app/api/send-email/route.ts
import { Resend } from "resend";

const resend = new Resend(process.env.RESEND_API_KEY);

export async function POST(request: Request) {
  const body = await request.json();
  const { name, email, message } = body;

  const { data, error } = await resend.emails.send({
    from: "Website <onboarding@resend.dev>",
    to: ["you@example.com"],
    subject: `New message from ${name}`,
    html: `
      <h2>New Contact Form Submission</h2>
      <p><strong>Name:</strong> ${name}</p>
      <p><strong>Email:</strong> ${email}</p>
      <p><strong>Message:</strong> ${message}</p>
    `,
  });

  if (error) {
    return Response.json({ error: error.message }, { status: 500 });
  }

  return Response.json({ success: true, data });
}

Connecting the Form

"use client";
import { useState } from "react";

export default function ContactForm() {
  const [isSending, setIsSending] = useState(false);

  async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const form = new FormData(event.currentTarget);
    setIsSending(true);

    try {
      const response = await fetch("/api/send-email", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          name: form.get("name"),
          email: form.get("email"),
          message: form.get("message"),
        }),
      });

      if (!response.ok) throw new Error("Failed to send email");
    } finally {
      setIsSending(false);
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" placeholder="Your name" />
      <input name="email" type="email" placeholder="Your email" />
      <textarea name="message" placeholder="Your message" />
      <button type="submit" disabled={isSending}>
        {isSending ? "Sending..." : "Send Message"}
      </button>
    </form>
  );
}

Validate the Input

Never trust data simply because it came from your own frontend. A malicious user can bypass the UI and call your endpoint directly. The server should validate the request:

import { z } from "zod";

const contactSchema = z.object({
  name: z.string().min(2),
  email: z.string().email(),
  message: z.string().min(10),
});

const result = contactSchema.safeParse(body);

if (!result.success) {
  return Response.json({ error: "Invalid form data" }, { status: 400 });
}

Deployment

Everything can work perfectly on localhost and then fail after deployment. Why? Because your local .env.local has RESEND_API_KEY and your production environment doesn't — yet. Configure the environment variable in your deployment platform, and never commit it to Git.

The Three Boundaries

LayerResponsibility
ClientCollect input · show loading states · show success/error feedback · basic UX validation
ServerValidate incoming data · protect secrets · control the email request · handle server-side errors
ResendProcess the email request · email delivery infrastructure

The goal isn't simply to make the email send. The goal is to build the entire flow correctly: User → UI → Request → Server → Validation → Resend → Email → User feedback.

The Mental Model

If you remember only one thing: Frontend says "I want to send this data." Next.js Server says "I can safely use the secret here." Resend says "I'll handle email delivery." That's the whole model.

Sometimes the engineering challenge isn't "How do I build an email delivery system?" It's "How do I integrate an existing service correctly into my application?" With Next.js and Resend, the basic implementation is remarkably small. The engineering is in getting all the layers right.

10 min read