All posts
    Tutorials

    Email Testing in Node.js with Jest

    The Devmailr Team6 min read

    When your Node.js app sends a verification email, a Jest or Vitest test should be able to confirm it actually went out — and read the code or link inside. Here’s a small, dependency-free way to do that against a disposable inbox.

    The idea

    Unlike browser e2e (Playwright/Cypress), here you’re testing your backenddirectly: call your “send verification email” code (or hit your API), then read the email that lands in a disposable inbox and assert on it. Node 18+ has fetch built in, so no client library is needed.

    A tiny helper

    devmailr.js
    const BASE = 'https://api.devmailr.app/api'
    const headers = { Authorization: `Bearer ${process.env.DEVMAILR_API_KEY}` }
    
    export async function createInbox(prefix) {
      const res = await fetch(`${BASE}/mailboxes`, {
        method: 'POST',
        headers: { ...headers, 'Content-Type': 'application/json' },
        body: JSON.stringify({ prefix }),
      })
      return res.json()
    }
    
    export async function waitForEmail(mailboxId, query = '') {
      const res = await fetch(`${BASE}/mailboxes/${mailboxId}/wait?timeout=60&${query}`, { headers })
      if (res.status === 404) throw new Error('No matching email arrived in time')
      return res.json()
    }
    
    export async function deleteInbox(mailboxId) {
      await fetch(`${BASE}/mailboxes/${mailboxId}`, { method: 'DELETE', headers })
    }

    The test (Jest or Vitest)

    Same API in both. Create an inbox, trigger your sender, assert on the email, clean up:

    email.test.js
    import { createInbox, waitForEmail, deleteInbox } from './devmailr'
    import { sendVerificationEmail } from '../src/email' // your app code
    
    let inbox
    beforeAll(async () => { inbox = await createInbox(`jest-${Date.now()}`) })
    afterAll(async () => { await deleteInbox(inbox._id) })
    
    test('sends a verification email with a 6-digit code', async () => {
      await sendVerificationEmail(inbox.address)
    
      const email = await waitForEmail(inbox._id, 'has_code=true')
    
      expect(email.from).toContain('noreply@myapp.com')
      expect(email.subject).toMatch(/verify/i)
      expect(email.extractedCodes[0]).toMatch(/^\d{6}$/)
    }, 65_000)

    Raise the timeout: /wait can block up to 60s, so bump the per-test timeout above it — the third arg 65_000 in Jest (as above), or { timeout: 65_000 } in Vitest. Otherwise the runner gives up before the email arrives.

    Testing a reset link

    javascript
    test('password reset email contains a working link', async () => {
      await sendPasswordReset(inbox.address)
      const email = await waitForEmail(inbox._id, 'subject=' + encodeURIComponent('reset'))
      const url = new URL(email.primaryLink)
      expect(url.searchParams.get('token')).toBeTruthy()
    }, 65_000)

    Tips

    • Node 18+ has fetch built in — no axios or node-fetch needed.
    • One inbox per file/run; clean it up in afterAll.
    • Always raise the test timeout above the wait timeout.
    • Pair this with browser e2e — see Playwright and Cypress for full-UI flows.

    Related reading

    Try it in two minutes

    Create a disposable prefix@devmailr.app inbox over the API, send mail to it from your app, and read it in your tests. Free plan, no card required.