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/sdkStep 2 — Set your API key
Add your API key to .env or .env.local:
MAILBEAM_KEY=mb_live_xxxxxxxxxxxxxxxxxxxxStep 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
| Practice | Why |
|---|---|
| Fail open on API errors | A Mailbeam outage shouldn't prevent user signups |
| Cache results (1h TTL) | Avoid duplicate verifications for the same address |
| Lowercase + trim before verify | Prevent cache misses from trivial differences |
| Use test domains in CI | Don't consume production quota in automated tests |
| Log errors, not emails | Avoid logging PII in production |
Production checklist
-
MAILBEAM_KEYset 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