Testing Express Apps
Integration testing API endpoints with supertest - testing routes without starting a real server, mocking the database layer, and what to actually test.
What it is, in plain English: Testing an Express app means sending real HTTP-like requests to your routes and asserting on the response -- status code, body shape, headers -- without actually binding to a network port. Supertest is the standard library for this: it wraps your Express app object directly, letting tests run fast and in-process. Combined with Vitest or Jest as the test runner, this is how Express APIs are tested in production codebases.
Testing routes without starting a real server
Step 1 -- structure your app for testability
// app.js -- defines the app, but does NOT start listening:
import express from "express";
const app = express();
app.use(express.json());
app.get("/health", (req, res) => res.json({ status: "ok" }));
export default app;// server.js -- the actual entry point, separate from app.js:
import app from "./app.js";
app.listen(3000, () => console.log("Running"));
// This split matters: tests import app.js and never touch server.js,
// so they never actually bind to a port.Step 2 -- install supertest
# npm install -D supertest vitestA basic test
import request from "supertest";
import { test, expect } from "vitest";
import app from "../app.js";
test("GET /health returns ok status", async () => {
const response = await request(app).get("/health");
expect(response.status).toBe(200);
expect(response.body).toEqual({ status: "ok" });
});Testing POST requests with a body
test("POST /users creates a user", async () => {
const response = await request(app)
.post("/users")
.send({ name: "Alice", email: "alice@example.com" });
expect(response.status).toBe(201);
expect(response.body).toMatchObject({ name: "Alice" });
expect(response.body.id).toBeDefined();
});
test("POST /users rejects missing email", async () => {
const response = await request(app)
.post("/users")
.send({ name: "Alice" }); // missing email
expect(response.status).toBe(400);
});Testing protected routes
test("GET /profile requires authentication", async () => {
const response = await request(app).get("/profile");
expect(response.status).toBe(401);
});
test("GET /profile works with a valid token", async () => {
const token = generateTestToken({ userId: 1 });
const response = await request(app)
.get("/profile")
.set("Authorization", `Bearer ${token}`);
expect(response.status).toBe(200);
});
Try It Yourself (Node.js logic)
Official Sources
The Codex links only to official documentation. No third-party tutorials.