Key Takeaways

  • In Next.js App Router, email verification belongs in a Server Action or Route Handler so the API key stays server-side and never reaches the browser bundle.
  • Never prefix the key with NEXT_PUBLIC_, which exposes it to client-side JavaScript. Read it from process.env inside server code only.
  • Use the HTTP-based verification API, not SMTP from a serverless function, which can exceed the default timeout on cold starts.
  • Validate server-side before the address is written to your database, and debounce client-side real-time checks at around 400 milliseconds.

Next.js is one of the most common stacks for modern signup flows, and the App Router has changed how server-side work is structured. As of 2026 the App Router is the stable, recommended pattern and the Pages Router is in maintenance mode. This guide builds email verification in Next.js with TypeScript the right way: a typed Server Action and Route Handler that call the v2 API server-side, keep your key secret, and gate the database write on the result. For the full reference, including other JavaScript runtimes, see the email verification integrations hub and the verify email with Node.js guide.

The most important rule comes first, because it is the one most often broken.

Important Never put your API key in a NEXT_PUBLIC_ variable or call the verification API from a Client Component. Anything with the NEXT_PUBLIC_ prefix is bundled into client-side JavaScript, which exposes your key to anyone who opens devtools. Keep all verification calls in server code.

The v2 Endpoint and Typed Response

Confirm the endpoint with curl, then model the response as a TypeScript interface so the rest of your code is type-safe.

curl -X GET 
  "https://emailverifierapi.com/v2/verify?api_key=YOUR_API_KEY&email=jane@example.com"

# Response
{
  "email": "jane@example.com",
  "status": "passed",
  "sub_status": "mailboxExists",
  "isDisposable": false,
  "isFreeService": false,
  "isRoleAccount": false,
  "isGibberish": false,
  "smtp_check": "success"
}

The status field is the primary signal: passed means the mailbox exists and accepts mail, failed means it does not, and unknown or transient covers greylisting and temporary errors. The booleans flag risk categories you may want to reject independently.

The Server-Side Verifier

Put the verification call in a server-only utility so it can be shared by both a Route Handler and a Server Action. The key is read from process.env and never leaves the server.

// lib/verifyEmail.ts  (server-only)
import "server-only";

export interface Verification {
  email: string;
  status: "passed" | "failed" | "unknown" | "transient";
  sub_status: string;
  isDisposable: boolean;
  isRoleAccount: boolean;
  isGibberish: boolean;
  smtp_check: "success" | "error";
}

export async function verifyEmail(email: string): Promise<Verification> {
  const key = process.env.EMAILVERIFIER_API_KEY;
  if (!key) throw new Error("Missing EMAILVERIFIER_API_KEY");

  const url = new URL("https://emailverifierapi.com/v2/verify");
  url.searchParams.set("api_key", key);
  url.searchParams.set("email", email);

  const res = await fetch(url, { cache: "no-store" });
  if (!res.ok) throw new Error(`Verify failed: ${res.status}`);
  return (await res.json()) as Verification;
}

The server-only import is a guardrail: if anyone accidentally imports this file into a Client Component, the build fails instead of silently shipping your key to the browser. The cache: no-store option keeps Next.js from caching a verification result that should always be fresh.

The Route Handler

The App Router uses named HTTP method exports in app/api/.../route.ts. This handler validates the address and returns a clean verdict the client can act on, without ever exposing the key.

// app/api/verify/route.ts
import { NextRequest, NextResponse } from "next/server";
import { verifyEmail } from "@/lib/verifyEmail";

export async function POST(req: NextRequest) {
  const { email } = await req.json();
  if (typeof email !== "string") {
    return NextResponse.json({ error: "email required" }, { status: 400 });
  }
  try {
    const r = await verifyEmail(email);
    const accept = r.status === "passed" && !r.isDisposable;
    return NextResponse.json({
      accept,
      status: r.status,
      reason: r.isDisposable ? "disposable" : r.sub_status,
    });
  } catch {
    // Fail open or closed per your risk tolerance; here we let the user retry
    return NextResponse.json({ accept: true, status: "unknown" });
  }
}
Validate on the server before the address ever reaches your database. Source: Next.js server-side validation best practice, 2026

The Server Action for Signup

For a form submission, a Server Action gates the database write directly, with no separate API route needed. The 'use server' directive marks the function as server-only.

// app/actions/signup.ts
"use server";
import { verifyEmail } from "@/lib/verifyEmail";

export async function signup(formData: FormData) {
  const email = String(formData.get("email") ?? "");
  const r = await verifyEmail(email);

  if (r.status === "failed" || r.isDisposable) {
    return { ok: false, message: "Please use a valid, non-disposable email." };
  }
  if (r.status === "unknown" || r.status === "transient") {
    return { ok: false, message: "Could not verify right now, try again." };
  }

  // status === passed: safe to persist
  // await db.user.create({ data: { email } });
  return { ok: true };
}

On the client, call the Route Handler on a 400 millisecond debounce as the user types so you show an inline checkmark or typo warning without a request on every keystroke. Sub-second verification means this adds no perceptible delay. Keep the final, authoritative check in the Server Action so the database write is always gated server-side, even if a bot bypasses the client.

New developers can grab 100 free email verification credits to test the flow, and the email verification API documentation covers the full response schema and rate limits.

Frequently Asked Questions

Should I use a Server Action or a Route Handler for email verification?

Use a Server Action for form submissions that write to your database, since it gates the write directly with no separate endpoint. Use a Route Handler when you need an HTTP endpoint for client-side real-time checks as the user types. Many apps use both: a debounced Route Handler call for UX and a Server Action for the authoritative final check.

Why should the API key never use the NEXT_PUBLIC_ prefix?

Any environment variable prefixed with NEXT_PUBLIC_ is inlined into the client-side JavaScript bundle, so the key becomes visible to anyone who inspects the page. Read the key from process.env inside server code only, and add a server-only import to your verifier module so an accidental client import fails the build.

Can I verify emails with SMTP from a Next.js serverless function?

It is not recommended. Raw SMTP connections can exceed the default serverless function timeout, especially on cold starts when the TCP connection is established from scratch. The HTTP-based verification API completes in a single HTTPS request, well within any serverless timeout, which makes it the reliable choice on platforms like Vercel.

How do I add real-time validation without hurting UX?

Debounce the client-side call to your Route Handler at about 400 milliseconds so it fires on a typing pause rather than every keystroke. With sub-second verification, the result returns before the user finishes, letting you show a green check or a typo suggestion inline with no perceptible delay.