Skip to main content
Back to Blog
Tutorials
3 min read
November 22, 2024

How to Build an Email Signup Form with Resend and React

Build an email signup form that sends welcome emails using Resend. Covers form handling, server actions, double opt-in, and email template design.

Ryel Banfield

Founder & Lead Developer

Collecting email addresses and sending professional emails is essential for any business website. Resend provides a modern, developer-friendly email API. Here is how to build a complete signup flow.

Step 1: Set Up Resend

  1. Create an account at resend.com
  2. Add and verify your domain (Settings > Domains)
  3. Create an API key (Settings > API Keys)
# .env.local
RESEND_API_KEY=re_123456789

Step 2: Install Dependencies

pnpm add resend

Step 3: Create the API Route

// app/api/subscribe/route.ts
import { NextResponse } from "next/server";
import { Resend } from "resend";
import { z } from "zod";

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

const subscribeSchema = z.object({
  email: z.string().email("Invalid email address"),
});

export async function POST(request: Request) {
  try {
    const body = await request.json();
    const result = subscribeSchema.safeParse(body);

    if (!result.success) {
      return NextResponse.json(
        { error: "Invalid email address" },
        { status: 400 }
      );
    }

    const { email } = result.data;

    // Add to Resend audience
    await resend.contacts.create({
      email,
      audienceId: process.env.RESEND_AUDIENCE_ID!,
    });

    // Send welcome email
    await resend.emails.send({
      from: "Your Company <hello@yourdomain.com>",
      to: email,
      subject: "Welcome to our newsletter",
      html: getWelcomeEmailHTML(),
    });

    return NextResponse.json({ success: true });
  } catch (error) {
    console.error("Subscribe error:", error);
    return NextResponse.json(
      { error: "Failed to subscribe" },
      { status: 500 }
    );
  }
}

function getWelcomeEmailHTML(): string {
  return `
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="utf-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
    </head>
    <body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; margin: 0; padding: 40px 20px; background-color: #f5f5f5;">
      <div style="max-width: 560px; margin: 0 auto; background-color: #ffffff; border-radius: 8px; padding: 40px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
        <h1 style="font-size: 24px; font-weight: 700; color: #0a0a0a; margin: 0 0 16px;">
          Welcome aboard
        </h1>
        <p style="font-size: 16px; line-height: 1.6; color: #525252; margin: 0 0 24px;">
          Thanks for subscribing. We share practical insights on web development, design, and technology β€” no spam, just useful content.
        </p>
        <p style="font-size: 16px; line-height: 1.6; color: #525252; margin: 0;">
          We publish weekly. Reply to this email anytime β€” we read every response.
        </p>
      </div>
      <p style="text-align: center; font-size: 12px; color: #a3a3a3; margin-top: 24px;">
        Your Company | yourdomain.com
      </p>
    </body>
    </html>
  `;
}

Step 4: Build the Signup Form

// components/newsletter/SignupForm.tsx
"use client";

import { useState } from "react";

export function NewsletterSignup() {
  const [email, setEmail] = useState("");
  const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle");
  const [errorMessage, setErrorMessage] = useState("");

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    setStatus("loading");
    setErrorMessage("");

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

      if (!response.ok) {
        const data = await response.json();
        throw new Error(data.error || "Failed to subscribe");
      }

      setStatus("success");
      setEmail("");
    } catch (error) {
      setStatus("error");
      setErrorMessage(error instanceof Error ? error.message : "Something went wrong");
    }
  }

  if (status === "success") {
    return (
      <div className="rounded-lg bg-green-50 p-4 dark:bg-green-900/20">
        <p className="font-medium text-green-800 dark:text-green-400">
          Check your inbox for a welcome email.
        </p>
      </div>
    );
  }

  return (
    <form onSubmit={handleSubmit} className="flex gap-2">
      <label htmlFor="email-signup" className="sr-only">
        Email address
      </label>
      <input
        id="email-signup"
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="you@example.com"
        required
        className="flex-1 rounded-md border px-3 py-2 text-sm"
        aria-describedby={status === "error" ? "signup-error" : undefined}
      />
      <button
        type="submit"
        disabled={status === "loading"}
        className="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50"
      >
        {status === "loading" ? "..." : "Subscribe"}
      </button>
      {status === "error" && (
        <p id="signup-error" className="mt-1 text-sm text-red-600" role="alert">
          {errorMessage}
        </p>
      )}
    </form>
  );
}

Step 5: Using React Email for Beautiful Templates

For more complex email templates, use React Email:

pnpm add @react-email/components
// emails/welcome.tsx
import {
  Body,
  Container,
  Head,
  Heading,
  Html,
  Preview,
  Text,
} from "@react-email/components";

export function WelcomeEmail({ name }: { name?: string }) {
  return (
    <Html>
      <Head />
      <Preview>Welcome to our newsletter</Preview>
      <Body style={{ fontFamily: "sans-serif", backgroundColor: "#f5f5f5", padding: "40px 0" }}>
        <Container style={{ backgroundColor: "#ffffff", borderRadius: "8px", padding: "40px", maxWidth: "560px" }}>
          <Heading style={{ fontSize: "24px", fontWeight: "700" }}>
            Welcome{name ? `, ${name}` : ""}
          </Heading>
          <Text style={{ fontSize: "16px", lineHeight: "1.6", color: "#525252" }}>
            Thanks for subscribing. We share practical insights on web development,
            design, and technology.
          </Text>
        </Container>
      </Body>
    </Html>
  );
}

Use it in your API route:

import { WelcomeEmail } from "@/emails/welcome";
import { render } from "@react-email/render";

const html = await render(WelcomeEmail({ name: "Sarah" }));

await resend.emails.send({
  from: "Your Company <hello@yourdomain.com>",
  to: email,
  subject: "Welcome to our newsletter",
  html,
});

Step 6: Add a Honeypot for Bot Protection

<form onSubmit={handleSubmit}>
  {/* Honeypot - hidden from real users, bots will fill it */}
  <input
    type="text"
    name="website"
    value=""
    onChange={() => {}}
    tabIndex={-1}
    autoComplete="off"
    style={{ position: "absolute", left: "-9999px" }}
    aria-hidden="true"
  />
  {/* Real fields */}
  <input type="email" ... />
  <button type="submit">Subscribe</button>
</form>

On the server, reject submissions where the honeypot field has a value.

Rate Limiting

Protect your endpoint:

// Simple in-memory rate limit
const submissions = new Map<string, number[]>();

function isRateLimited(ip: string): boolean {
  const now = Date.now();
  const windowMs = 60 * 1000; // 1 minute
  const maxRequests = 3;

  const timestamps = submissions.get(ip)?.filter((t) => now - t < windowMs) || [];
  timestamps.push(now);
  submissions.set(ip, timestamps);

  return timestamps.length > maxRequests;
}

Need Email Marketing Integration?

We set up email collection, automated sequences, and transactional emails for business websites. Contact us for email marketing development.

emailResendnewsletterReacttutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles