Supertest & HTTP Integration
Supertest sends HTTP requests to Express and Fastify apps in-process without binding a port, making integration tests fast and parallel-safe.
Recipe
Quick-reference recipe card - copy-paste ready.
import request from "supertest";
import { createApp } from "../src/app.js";
const res = await request(createApp()).get("/health");npm install -D supertest @types/supertestWhen to reach for this:
- Testing routes, middleware order, and status codes.
- Validating JSON request/response bodies and auth headers.
- Avoiding flaky port collisions in parallel CI.
Working Example
// src/app.ts
import express from "express";
export function createApp() {
const app = express();
app.use(express.json());
app.get("/health", (_req, res) => {
res.json({ status: "ok" });
});
app.post("/items", (req, res) => {
if (!req.body?.name) {
return res.status(400).json({ error: "name required" });
}
res.status(201).json({ id: "item_1", name: req.body.name });
});
return app;
}// test/app.http.test.ts
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import request from "supertest";
import { createApp } from "../src/app.js";
describe("HTTP API", () => {
it("GET /health returns ok", async () => {
const res = await request(createApp()).get("/health");
assert.equal(res.status, 200);
assert.deepEqual(res.body, { status: "ok" });
});
it("POST /items validates body", async () => {
const res = await request(createApp())
.post("/items")
.send({})
.set("Content-Type", "application/json");
assert.equal(res.status, 400);
assert.equal(res.body.error, "name required");
});
});What this demonstrates:
createApp()factory avoidslisten()side effects.- Supertest works with
node:test(no Jest required). - Assert on
res.bodyparsed JSON, not raw strings.
Deep Dive
How It Works
- Supertest wraps the Node HTTP server and injects requests via internal socket.
- No network port means tests run in parallel without
EADDRINUSE. - Middleware stack executes identically to production until you mock dependencies.
- Fastify: use
app.inject()natively or Supertest afterapp.ready().
Fastify inject Alternative
import Fastify from "fastify";
const app = Fastify();
app.get("/health", async () => ({ ok: true }));
await app.ready();
const res = await app.inject({ method: "GET", url: "/health" });
// res.statusCode, res.json()TypeScript Notes
- Express 5 types ship with the package on Node 24 stacks.
- Type
createApp()return asexpress.ApplicationorFastifyInstancefor editor help.
Gotchas
- Calling listen() in app module - Port binds on import; tests fight production server. Fix: export factory; listen only in
server.ts. - Shared mutable DB singleton - Parallel tests corrupt data. Fix: per-test transaction rollback or in-memory repo in unit HTTP tests.
- Missing await on request - Supertest returns Test; must
awaitpromise in async tests. Fix: alwaysawait request(...).get(). - Cookie/session state leaking - Agent persists cookies between calls unintentionally. Fix:
request.agent(app)only when testing sessions; freshrequest(app)otherwise. - Testing only happy path - 500 errors unhandled. Fix: table-driven tests for 400/401/404/409 cases.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
Fastify inject | Fastify-only codebase | Express apps |
| Real port + fetch | Testing TLS or HTTP/2 edge | Default integration |
| Pact contract tests | Consumer-provider boundary | Single monolith API |
FAQs
Does Supertest work with Express 5?
Yes. Supertest attaches to Express Application HTTP handler as in Express 4.
How do I test authenticated routes?
await request(app).get("/me").set("Authorization", "Bearer test-token");Use test tokens or override auth middleware in createApp({ auth: "test" }).
Can I test multipart uploads?
Supertest supports .attach() for multipart form data.
How do I test WebSockets?
Supertest is HTTP-only. Use ws client against test server or dedicated WS test utils.
NestJS e2e?
@nestjs/testing creates app; use Supertest request(app.getHttpServer()).
Why not hit localhost:3000?
Port binding is slower, flaky in parallel CI, and requires separate process lifecycle management.
How do I mock the database?
Inject repository mocks when calling createApp({ repos: mocks }) - avoid mocking at HTTP layer only.
Does order of middleware matter?
Yes - integration tests catch auth-before-parser mistakes Supertest exercises full stack.
How to test streaming responses?
Assert on res.text or stream events; more complex - consider focused unit tests on stream handlers.
Supertest with node:test or Vitest?
Both work; choose repo standard runner; Supertest API unchanged.
Related
- Testing Basics - pyramid placement for HTTP tests
- Testcontainers - real DB behind HTTP layer
- Vitest - runner with mock ergonomics
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.