Testing Handlers

Testing HTTP Handlers and Data Access

Answering HTTP requests and talking to a database are the two things people stop testing first, because both look like they need a running environment. Neither does. Bind the server to port 0 and the operating system hands you a free port, so many test files can run in parallel without colliding. Start it in before, close it in after, and use the global fetch: nothing is stubbed, and the real routing and status codes run.

Testing an HTTP handler over a real socketJavaScript
import { once } from 'node:events';
import { DatabaseSync } from 'node:sqlite';
describe('the products API', () => {
  let db, server, base;
  before(async () => {
    db = new DatabaseSync(':memory:');
    server = createApp(createRepo(db));
    server.listen(0);
    await once(server, 'listening');
    base = `http://127.0.0.1:${server.address().port}`;
  });
  after(() => { server.close(); db.close(); });
  it('stores a product and reads it back', async () => {
    const res = await fetch(`${base}/products`, {
      method: 'POST', body: JSON.stringify({ name: 'Kettle', price: 2400 }),
    });
    assert.equal(res.status, 201);
    const list = await fetch(`${base}/products?q=Ket`).then((r) => r.json());
    assert.deepEqual(list, [{ name: 'Kettle', price: 2400 }]);
  });
});
Output
▶ the products API
  ✔ stores a product and reads it back (28.2161ms)
✔ the products API (33.4273ms)
ℹ tests 1
ℹ pass 1

A sibling test posting { price: 900 } and asserting a 422 with { error: 'name is required' } covers the validation path.

The data layer is node:sqlite with a :memory: database: a real SQL engine, built into Node, created and destroyed per suite, with nothing left on disk. createRepo(db) prepares the statements the handler needs and returns { list, add }, so the handler depends on an object rather than on a driver — that seam is what lets it run against SQLite 4,756 here and the real driver in production. node:sqlite is stability 1.2, release candidate, on Node 25 and 26.

The same shape works for Mongo, except that an in-memory substitute is never quite the database. mongodb-memory-server 2,853 (https://github.com/typegoose/mongodb-memory-server 2,853 ) (npm 2,036 i -D mongodb-memory-server) downloads a real mongod binary on first use and starts it on a random port against a temporary directory; a container is the only honest choice for transactions, indexes and aggregation pipelines. A database server is too slow to start per file, so boot it once in a global setup module exporting globalSetup (call MongoMemoryServer.create(), put mongod.getUri() into process.env) and globalTeardown, then run node --test --test-global-setup=./test/setup.mjs; that module runs once before the first test file and its teardown after the last. The flag is stability 1.0, early development. Never point a suite at a shared development database: a beforeEach that clears a collection is one environment variable away from clearing the wrong one. Express.js rebuilds these tests against Express 24,430 and MongoDB with Mongoose 243,355 models.