Guides /

Express

JS route middleware

A middleware you put in front of the signup route. Bad addresses get a 422 and your handler never runs.

The code

1 Keep the key on the server

MAILVET_KEY=mv_live_your_key

Set it wherever your server reads its environment. Never put it in client code.

2 Write the middleware

mailvet.js

// Needs Node 18+ for fetch. Your route must parse the body first.
export async function vetEmail(req, res, next) {
  const email = String(req.body?.email ?? "").trim();
  if (!email) return next();

  let check;
  try {
    const answer = await fetch(
      "https://mailvet.dev/v1/check?email=" + encodeURIComponent(email),
      {
        headers: { Authorization: `Bearer ${process.env.MAILVET_KEY}` },
        signal: AbortSignal.timeout(5000),
      },
    );
    if (!answer.ok) throw new Error(`HTTP ${answer.status}`);
    check = await answer.json();
  } catch (err) {
    console.error("mailvet: no answer:", err.message);
    return next(); // a signup shouldn't die because of us
  }

  if (check.did_you_mean) {
    const local = email.slice(0, email.lastIndexOf("@"));
    return res.status(422).json({
      error: `Did you mean ${local}@${check.did_you_mean}?`,
      did_you_mean: check.did_you_mean,
    });
  }
  if (check.verdict === "invalid") {
    return res.status(422).json({ error: "That address can't receive mail." });
  }
  if (check.verdict === "risky" && check.disposable) {
    return res.status(422).json({ error: "Please use a permanent address." });
  }
  next();
}

3 Put it on the route

app.js

import express from "express";
import { vetEmail } from "./mailvet.js";

const app = express();

app.post("/signup", express.json(), vetEmail, (req, res) => {
  // Create the user here.
  res.status(201).json({ ok: true });
});

app.listen(process.env.PORT ?? 3000);

What to do with the answer

did_you_mean, invalid and disposable end the request with a 422 and a message your form can show. Everything else reaches your handler, including role addresses and a risky from a DNS failure.

  • valid Let it through.
  • risky Read disposable. If it’s true, reject. If false, it’s a role address or DNS couldn’t be reached, so let it through.
  • invalid Bad format, or the domain has no mail server. Reject.

did_you_mean is empty or holds a domain, such as gmail.com for gamil.com. It doesn’t change the verdict. A known typo can come back valid because squatters give those domains a mail server, so check it first. See the docs for every field.

If MailVet is down, the signup goes through

The try covers timeouts, network errors, non-200 answers and bad JSON. All of them log and call next(), so your handler runs as if the check had passed.

A 401 or a 429 takes the same path, so a bad key or a used-up quota means addresses pass unchecked. Watch your logs for it.

Watch out

  • Both files use ES modules, so set "type": "module" in package.json.
  • Handling a form post instead of JSON? Swap express.json() for express.urlencoded({ extended: false }).

Sources

Compared with these on November 5, 2026. If they’ve changed since, trust them over this page.

Other guides

Get API key