Validate email on a signup form with a server-side API

By the 1-email.com editorial team · Reviewed October 9, 2026 · Editorial policy

Connect a browser form to a local Node.js backend without putting a live API key in browser code.

Download and run locally

Node.js 22+. No package installation is needed. Download every file below into the same directory, then run:

node signup-server.mjs

Open http://127.0.0.1:3000. The browser calls your local backend; the backend calls the API. This example creates no accounts and stores no contacts.

Source files

verify.mjs Download

// Node.js 22+: server code. Only the public sandbox key is supplied by default.
export class VerificationError extends Error {
  constructor(status, message, retryAfter = null) {
    super(message); this.status = status; this.retryAfter = retryAfter;
  }
}
export async function verifyEmail(email, {key = process.env.EEV_API_KEY || 'eev_sandbox_key', fetchFn = fetch} = {}) {
  const url = new URL('https://api.easyemailverification.com/v1/verify');
  url.searchParams.set('email', email);
  let response;
  try {
    response = await fetchFn(url, {
      headers: {'X-API-Key': key, Accept: 'application/json'},
      signal: AbortSignal.timeout(30000)
    });
  } catch {
    throw new VerificationError(503, 'No reliable result; review the request before retrying.');
  }
  if (!response.ok) {
    const messages = {402: 'No credits: check your account before retrying.',
      429: 'Rate limited: wait before another request; respect Retry-After.'};
    throw new VerificationError(response.status, messages[response.status] || 'Verification request failed.', response.headers.get('Retry-After'));
  }
  let data;
  try { data = await response.json(); } catch { throw new VerificationError(502, 'Invalid JSON response.'); }
  if (!data || !['valid','invalid','unknown'].includes(data.result)) {
    throw new VerificationError(502, 'Unexpected API response; do not classify the address.');
  }
  return data;
}
export function classify(data) {
  if (data.result === 'invalid') return 'invalid';
  if (data.result === 'unknown' || data.disposable || data.accept_all || data.safe_to_send !== true) return 'review';
  return 'passed_checks';
}

signup-server.mjs Download

// Local teaching example; creates no accounts and stores no submitted addresses.
import {createServer} from 'node:http';
import {readFile} from 'node:fs/promises';
import {verifyEmail, classify} from './verify.mjs';
const form = await readFile(new URL('./signup.html', import.meta.url));
const server = createServer(async (req, res) => {
  const send = (status, body) => {
    res.writeHead(status, {'Content-Type': 'application/json', 'Cache-Control':'no-store'});
    res.end(JSON.stringify(body));
  };
  if (req.method === 'GET' && req.url === '/') {
    res.writeHead(200, {'Content-Type':'text/html; charset=utf-8'}); return res.end(form);
  }
  if (req.method !== 'POST' || req.url !== '/verify') return send(404, {message:'Not found'});
  if ((req.headers['content-type'] || '').split(';')[0] !== 'application/json') return send(415, {message:'Use JSON'});
  let body = ''; let bytes = 0;
  try {
    for await (const chunk of req) {
      bytes += chunk.length;
      if (bytes > 2048) return send(413, {message:'Request too large'});
      body += chunk.toString('utf8');
    }
    let input;
    try { input = JSON.parse(body); } catch { return send(400, {message:'Invalid JSON'}); }
    if (typeof input?.email !== 'string' || input.email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input.email)) {
      return send(400, {message:'Enter one complete address'});
    }
    const data = await verifyEmail(input.email);
    // Keep unknown and catch-all separate from a confirmed rejection.
    send(200, {decision: classify(data), result: data.result, reason: data.reason,
      did_you_mean: data.did_you_mean || null});
  } catch (error) {
    if (!res.headersSent) send(error.status || 503, {message:error.message || 'Check unavailable'});
  }
});
server.requestTimeout = 35000;
server.listen(3000, '127.0.0.1', () => console.log('Demo: http://127.0.0.1:3000'));

signup.html Download

<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Signup verification sandbox demo</title>
<style>body{font:18px system-ui;max-width:650px;margin:3rem auto;padding:1rem}input,button{font:inherit;padding:.6rem}pre{white-space:pre-wrap}</style>
<h1>Try a signup check</h1><p>This local demo creates no accounts. Start with a documented sandbox address.</p>
<form id="form"><label for="email">Email address</label><input id="email" name="email" type="email" required value="valid@sandbox.easyemailverification.com"><button>Check</button></form>
<pre id="result" role="status" aria-live="polite"></pre>
<script>
const form = document.getElementById('form'), output = document.getElementById('result');
form.addEventListener('submit', async event => {
  event.preventDefault(); const button = form.querySelector('button'); button.disabled = true;
  output.textContent = 'Checking…';
  try {
    const response = await fetch('/verify', {method:'POST', headers:{'Content-Type':'application/json'},
      body:JSON.stringify({email:document.getElementById('email').value})});
    const result = await response.json();
    output.textContent = `HTTP ${response.status}\n${JSON.stringify(result, null, 2)}`;
  } catch { output.textContent = 'Check unavailable. This is not proof of an invalid address.'; }
  finally { button.disabled = false; }
});
</script></html>

Test success, uncertainty and failures

Replace the default test address using the command-line argument (Python/Node) or the form field (signup). Use valid, invalid, unknown, catchall, disposable, quota and ratelimit at sandbox.easyemailverification.com. The special typo@gmial.com fixture gives a correction suggestion.

Expect unknown and risk flags to produce review rather than rejection. Quota should stop the request with 402; a rate limit returns 429. Network or JSON failures should never create a valid or invalid mailbox classification.

Before production

Set EEV_API_KEY in the server environment. Keep the endpoint fixed, set a timeout, limit request size and add abuse controls. For a real signup flow add origin/CSRF protection, your own rate limit and consent handling. Do not retry an uncertain request automatically or silently replace a typo.

Primary source: Easy API reference. Download the example instructions.

Ready for real addresses?

We run Easy Email Verification. Create a free account for a live key after testing your integration.

Get a free live key