Key Takeaways
  • Verifying at signup costs less than verifying after a bounce. The right place to call the verification API is in your signup middleware.
  • Express middleware is the cleanest pattern: validate before user creation, return structured errors to the frontend, log results.
  • The v2 verify endpoint returns status, sub_status, and risk flags. Decision logic should branch on combinations, not status alone.
  • Production deployments need three additions: caching, retry logic for transient errors, and graceful degradation if the API is briefly unavailable.

Verifying email addresses at signup is one of those changes that pays for itself within a week. Every disposable address you reject is a future bounce avoided. Every gibberish typo you flag is a customer support ticket avoided. Every role account you screen is a marketing list that stays clean. The only question is how to wire it into a Node.js application without making signup feel slow or breaking when the verification API is briefly unavailable.

This guide walks through the Express middleware pattern that handles all three concerns: speed, accuracy, and resilience. The complete reference implementation, including caching and retry, is documented on the verify email with Node.js integration page.

The v2 Endpoint and Response Fields

Before writing middleware, confirm the endpoint works against a known address from any shell. The v2 verify endpoint accepts the email as a query parameter and returns a JSON document with all the fields the application needs to make a decision.

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,
  "isOffensive": false,
  "isRoleAccount": false,
  "isGibberish": false,
  "smtp_check": "success"
}

The status field is the primary decision input. Passed indicates the mailbox exists and accepts mail. Failed means the mailbox does not exist, syntax is invalid, the domain has no MX server, or the address is otherwise unrouteable. Unknown covers greylisting and transient SMTP errors that warrant retry. Transient indicates a temporary issue worth deferring.

The boolean flags are the second decision input. isDisposable identifies addresses from temporary mail services. isRoleAccount flags shared mailboxes like info@ or sales@. isGibberish catches keyboard-mash typos. Each flag should map to a different rejection message so the frontend can show appropriate guidance.

The Express Middleware

The middleware wraps the HTTP call, parses the response, and attaches a structured verification result to the request object. Downstream handlers read the result and either proceed with user creation or return a 422 with a specific reason.

// middleware/verifyEmail.js
const VERIFY_URL = "https://emailverifierapi.com/v2/verify";
const API_KEY = process.env.EMAIL_VERIFIER_API_KEY;
const TIMEOUT_MS = 5000;

async function verifyEmail(req, res, next) {
  const email = req.body.email;
  if (!email) {
    return res.status(400).json({ error: "email required" });
  }

  const url = new URL(VERIFY_URL);
  url.searchParams.set("api_key", API_KEY);
  url.searchParams.set("email", email);

  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);

  try {
    const response = await fetch(url, { signal: controller.signal });
    clearTimeout(timer);

    if (!response.ok) {
      throw new Error(`API returned ${response.status}`);
    }

    const result = await response.json();
    req.emailVerification = result;

    // Reject obvious failures up front
    if (result.status === "failed") {
      return res.status(422).json({
        error: "email_invalid",
        reason: result.sub_status
      });
    }

    if (result.isDisposable) {
      return res.status(422).json({
        error: "disposable_email",
        reason: "Please use a non-disposable email address"
      });
    }

    if (result.isGibberish) {
      return res.status(422).json({
        error: "email_typo",
        reason: "Please double-check your email address"
      });
    }

    next();
  } catch (err) {
    clearTimeout(timer);
    // Graceful degradation: log and proceed
    console.error("Verification failed:", err.message);
    req.emailVerification = { status: "unverified", error: err.message };
    next();
  }
}

module.exports = verifyEmail;

Two design decisions deserve attention. First, the middleware returns 422 for clear rejections (invalid, disposable, gibberish) but proceeds on transient errors. The application stays available even if the verification API is briefly slow. Second, the verification result is attached to the request so downstream handlers can store it on the user record for later analysis.

Pro Tip

Always pass an AbortController with a 5-second timeout. Without it, a slow upstream can stall your signup form indefinitely. Five seconds is generous for the v2 endpoint, which typically returns in under 600 milliseconds.

Mounting the Middleware on the Signup Route

With the middleware in place, the signup route uses it as a gate before user creation. The verification result is available on req.emailVerification for any downstream logging or analytics.

// routes/signup.js
const express = require("express");
const verifyEmail = require("../middleware/verifyEmail");
const router = express.Router();

router.post("/signup", verifyEmail, async (req, res) => {
  const { email, name, password } = req.body;
  const verification = req.emailVerification;

  const user = await db.users.create({
    email,
    name,
    password_hash: await hash(password),
    email_status: verification.status,
    email_verified_at: new Date(),
    is_role_account: verification.isRoleAccount || false,
    is_free_service: verification.isFreeService || false
  });

  res.status(201).json({ id: user.id });
});

module.exports = router;
Stat Highlight
85% reduction
in welcome-email bounce rates after enabling pre-signup verification, measured across SaaS deployments using the v2 API.

Adding a Cache

The same email is often submitted multiple times during a single signup session: form submission failures, validation retries, browser back-button replays. A short-lived cache avoids paying for repeated verifications of the same address.

An in-memory LRU cache with a 30-minute TTL is enough for most applications. Redis is appropriate when running multi-instance Node.js. The cache key is the lowercased email. The cache value is the full verification response. Cache hits skip the API call entirely and return the stored result.

Bulk Verification for Existing Users

Real-time verification handles new signups. Existing users predate the middleware and need bulk verification. The bulk endpoint accepts a list and returns asynchronously through a webhook or polling, depending on configuration. For ongoing list health, schedule a quarterly bulk run against your active user base. The email verification API documentation covers both the real-time and bulk patterns with full request and response schemas.

Frequently Asked Questions

Should I verify on the frontend or backend?

Backend. API keys belong on the server. Frontend verification exposes the key in the page source and rate-limits cannot protect it. The middleware pattern keeps the key server-side and returns clean error codes to the frontend.

What status should block signup?

Block on status equals failed and on isDisposable equals true. Allow status equals passed unconditionally. For status equals unknown, the safer choice is to allow the signup and verify again asynchronously after the user activates.

How do I handle role accounts at signup?

Allow role accounts (info@, sales@) but flag them in the user record. Marketing campaigns should suppress role accounts because they generate higher complaint rates than personal addresses. Transactional mail to role accounts is normally fine.

What happens if the verification API is down?

Graceful degradation. The middleware logs the error, marks the user record as unverified, and allows signup. A scheduled job re-verifies unverified records the next time the API is available. The application stays online either way.