Mailbeam
Node.js + ExpressBeginner15 minutesUpdated January 2025

Email Verification in Node.js

In this tutorial, you'll add real-time email verification to a Node.js Express application. By the end, disposable emails, non-existent mailboxes, and typo'd addresses will be rejected at the signup endpoint before any user record is created.

What you'll build

A reusable Express middleware that:

  • Verifies an email address via the Mailbeam API
  • Returns a 422 with a machine-readable error code on failure
  • Fails open on API errors (so a Mailbeam outage doesn't break your signup)
  • Optionally caches results to avoid duplicate verifications

Prerequisites

  • Node.js 18 or later
  • An npm, pnpm, or yarn project
  • A Mailbeam account and API key (sign up free)

Step 1 — Install the SDK

npm install @mailbeam/sdk
# or
pnpm add @mailbeam/sdk

Step 2 — Set your API key

Add your API key to .env or .env.local:

MAILBEAM_KEY=mb_live_xxxxxxxxxxxxxxxxxxxx

Step 3 — Create the verification middleware

Create middleware/verifyEmail.js:

import Mailbeam from "@mailbeam/sdk";

// Initialize once at module level — re-using the same instance avoids
// re-authenticating on every request.
const mb = new Mailbeam({ apiKey: process.env.MAILBEAM_KEY });

/**
 * Express middleware that verifies the email in req.body.email.
 * Calls next() on success, returns 422 on failure.
 * Fails open on Mailbeam API errors — don't block users if the API is down.
 */
export async function verifyEmail(req, res, next) {
  const { email } = req.body;

  if (!email) {
    return res.status(400).json({ error: "email is required." });
  }

  try {
    const { valid, score, reason } = await mb.verify(email);

    if (!valid || score < 60) {
      return res.status(422).json({
        error: "Please provide a valid email address.",
        code: reason ?? "invalid_email",
      });
    }

    // Attach the result to the request so downstream handlers can use it
    req.emailVerification = { valid, score, reason };
    next();
  } catch (err) {
    // Log but don't block — a Mailbeam outage shouldn't break your signup
    console.error("[Mailbeam] verification error:", err.message);
    next();
  }
}

Step 4 — Add to your signup route

// routes/auth.js
import express from "express";
import { verifyEmail } from "../middleware/verifyEmail.js";

const router = express.Router();

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

  try {
    const user = await db.createUser({ email, password });
    res.status(201).json({ user });
  } catch (err) {
    res.status(500).json({ error: "Failed to create account." });
  }
});

export default router;

And your Express app setup:

// app.js
import express from "express";
import authRoutes from "./routes/auth.js";

const app = express();
app.use(express.json());
app.use("/api/auth", authRoutes);

app.listen(3000, () => {
  console.log("Server running on http://localhost:3000");
});

Testing the integration

Start your server, then test with cURL:

# Valid email — should return 201
curl -X POST http://localhost:3000/api/auth/signup \
  -H "Content-Type: application/json" \
  -d '{"email": "valid@example.com", "password": "secretpass"}'

# Disposable email — should return 422
curl -X POST http://localhost:3000/api/auth/signup \
  -H "Content-Type: application/json" \
  -d '{"email": "throwaway@mailinator.com", "password": "secretpass"}'

Use Mailbeam's test domains in your CI pipeline to avoid counting against your quota:

# Always returns valid (score 99)
curl ... -d '{"email": "user@valid.mailbeam-test.dev", ...}'

# Always returns invalid
curl ... -d '{"email": "user@invalid.mailbeam-test.dev", ...}'

Adding a cache layer (optional)

For high-traffic signups, cache verification results to avoid redundant API calls:

import { LRUCache } from "lru-cache";

const cache = new LRUCache({
  max: 500,
  ttl: 1000 * 60 * 60, // 1 hour
});

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

  const normalised = email.toLowerCase().trim();
  const cached = cache.get(normalised);

  if (cached) {
    if (!cached.valid || cached.score < 60) {
      return res.status(422).json({
        error: "Please provide a valid email address.",
        code: cached.reason ?? "invalid_email",
      });
    }
    return next();
  }

  try {
    const result = await mb.verify(normalised);
    cache.set(normalised, result);

    if (!result.valid || result.score < 60) {
      return res.status(422).json({
        error: "Please provide a valid email address.",
        code: result.reason ?? "invalid_email",
      });
    }
    next();
  } catch (err) {
    console.error("[Mailbeam] verification error:", err.message);
    next();
  }
}

Best practices

PracticeWhy
Fail open on API errorsA Mailbeam outage shouldn't prevent user signups
Cache results (1h TTL)Avoid duplicate verifications for the same address
Lowercase + trim before verifyPrevent cache misses from trivial differences
Use test domains in CIDon't consume production quota in automated tests
Log errors, not emailsAvoid logging PII in production

Production checklist

  • MAILBEAM_KEY set as a platform secret (not in source code)
  • Error logging configured (don't log the email address itself)
  • Cache layer added for high-traffic routes
  • Test domains used in CI pipeline
  • Score threshold decided and documented (default: 60)
  • Frontend shows a clear error message when 422 is returned

Next steps