Skip to content
Node.js6 min read

Testing Node.js APIs with Vitest and Supertest

Fast unit tests, honest integration tests and a database strategy that does not lie to you. How we test Node APIs with Vitest and Supertest.

The API test suites that actually catch bugs share a shape: a broad base of fast unit tests on business logic, a substantial layer of integration tests that exercise real routes against a real database, and almost nothing mocked in between. Vitest gives us the speed and Supertest gives us the realism, and together they make that shape cheap to maintain.

Supertest without the network

Supertest takes your app instance and dispatches requests to it in-process — no port binding, no server lifecycle, no flaky localhost races. The precondition is architectural: export the configured app separately from the file that calls listen, so tests import the former.

ts
import { describe, it, expect } from 'vitest';
import request from 'supertest';
import { buildApp } from '../src/app';

describe('POST /users', () => {
  it('rejects an invalid email', async () => {
    const app = buildApp();
    const res = await request(app)
      .post('/users')
      .send({ email: 'not-an-email', name: 'Ada' });
    expect(res.status).toBe(400);
    expect(res.body.error.field).toBe('email');
  });
});

Note what this test covers that a unit test cannot: routing, body parsing, the validation layer, and the error envelope your clients actually receive. Fastify users get the same effect with the built-in app.inject, no Supertest required. Testing through the HTTP surface also makes refactors cheap: services and repositories can be reshaped freely underneath while the suite keeps asserting the only contract your clients ever see.

The database question, answered plainly

Mocking the database is where API suites go to die: the mocks drift from real query behaviour and the suite turns green while production burns. Run a real database in tests. Testcontainers spins up a throwaway Postgres per suite run; on CI, a service container does the same job. For isolation between tests, truncating tables in a beforeEach is simple and fast enough for most suites, and wrapping each test in a rolled-back transaction is faster still when your stack allows it.

  • Mock only true externals: payment providers, email senders, third-party HTTP APIs.
  • Build test data with factory functions with sensible defaults, not shared fixtures that every test secretly depends on.
  • Assert on observable behaviour — status, body, database state — not on which internal functions were called.

Vitest settings that matter for APIs

Vitest runs test files in parallel by default, which is superb until two files truncate the same table mid-flight. Either give each worker its own schema or database, or set fileParallelism to false for the integration project and keep full parallelism for units. The projects feature makes this split explicit: a unit project with no setup that runs in watch mode as you type, and an integration project with the container lifecycle that runs on save and in CI. That separation keeps the feedback loop honest — milliseconds for logic, seconds for wiring.

Two further habits pay for themselves quickly. Fake time deliberately: anything touching Date.now or timers gets vi.useFakeTimers, so token-expiry and scheduling tests are exact rather than sleepy — a suite that waits on real setTimeout calls is a flake factory. And treat flakiness as a defect with an owner: a quarantine list reviewed weekly, not a culture of pressing retry. The first time the team stops trusting a red build, the suite has lost most of its value regardless of coverage.

Coverage targets we treat as advisory; the number that matters is how often the suite catches a real regression before review does. A hundred integration tests against a real database, in our experience, out-detect a thousand tests against mocks.

If your test suite is green but production keeps surprising you, that is a solvable problem — STRCLI can help you rebuild confidence in it.

Start your project

Have an idea? Let's ship it together.

Tell us what you're building — we'll reply within one business day with an honest take and a clear next step.