Skip to main content
Technology & EngineeringApi Testing226 lines

Supertest

Supertest for Node.js HTTP assertion testing with Express, Koa, and Fastify

Quick Summary31 lines
You are an expert in Supertest for testing HTTP APIs in Node.js applications.

## Key Points

- Always separate the Express/Koa/Fastify app from the server listener so Supertest can bind it in-process.
- Use `request.agent(app)` when testing flows that rely on cookies or sessions.
- Test both the happy path and meaningful error cases (validation, auth, not found).
- Assert on response body structure, not just status codes — a 200 with the wrong shape is still a bug.
- Set up and tear down test data per test or per suite to avoid ordering dependencies.
- Use factory functions or fixtures for test data rather than relying on seed data alone.
- Calling `app.listen()` inside the app module — Supertest needs the app without a running server.
- Forgetting `await` on Supertest chains — tests will pass vacuously because assertions never run.
- Not closing database connections in `afterAll`, causing Jest to hang with open handles.
- Using a shared mutable database without isolation — tests interfere with each other when run in parallel.
- Asserting on exact object equality when the response includes generated fields like `id` or `createdAt` — use `expect.objectContaining()` or `toMatchObject()` instead.

## Quick Example

```bash
npm install --save-dev supertest
# With TypeScript
npm install --save-dev supertest @types/supertest
```

```typescript
// src/server.ts
import app from "./app";
app.listen(3000, () => console.log("Running on :3000"));
```
skilldb get api-testing-skills/SupertestFull skill: 226 lines
Paste into your CLAUDE.md or agent config

Supertest — API Testing

You are an expert in Supertest for testing HTTP APIs in Node.js applications.

Core Philosophy

Overview

Supertest is a high-level HTTP testing library built on superagent. It enables in-process testing of Node.js HTTP servers without needing to start them on a real port, making tests fast and deterministic. It integrates naturally with test runners like Jest, Mocha, and Vitest.

Setup & Configuration

Installation

npm install --save-dev supertest
# With TypeScript
npm install --save-dev supertest @types/supertest

Basic project structure

src/
  app.ts          # Express app (exported, no .listen())
  server.ts       # Calls app.listen() for production
test/
  users.test.ts
  auth.test.ts
  helpers/
    setup.ts

Separating app from server

This is the key pattern — export the app without calling .listen():

// src/app.ts
import express from "express";
import { userRouter } from "./routes/users";

const app = express();
app.use(express.json());
app.use("/api/users", userRouter);

export default app;
// src/server.ts
import app from "./app";
app.listen(3000, () => console.log("Running on :3000"));

Core Patterns

Basic request and assertions

import request from "supertest";
import app from "../src/app";

describe("GET /api/users", () => {
  it("returns a list of users", async () => {
    const res = await request(app)
      .get("/api/users")
      .expect("Content-Type", /json/)
      .expect(200);

    expect(res.body).toBeInstanceOf(Array);
    expect(res.body.length).toBeGreaterThan(0);
    expect(res.body[0]).toHaveProperty("id");
    expect(res.body[0]).toHaveProperty("email");
  });
});

POST with body and auth header

it("creates a new user", async () => {
  const newUser = { name: "Alice", email: "alice@example.com" };

  const res = await request(app)
    .post("/api/users")
    .set("Authorization", `Bearer ${authToken}`)
    .send(newUser)
    .expect(201);

  expect(res.body.name).toBe("Alice");
  expect(res.body).toHaveProperty("id");
});

Authenticated test helper

// test/helpers/setup.ts
import request from "supertest";
import app from "../../src/app";

export async function authenticatedAgent() {
  const agent = request.agent(app);
  await agent
    .post("/api/auth/login")
    .send({ email: "test@test.com", password: "password123" });
  return agent; // agent retains cookies across requests
}
// test/users.test.ts
import { authenticatedAgent } from "./helpers/setup";

describe("Protected routes", () => {
  let agent: ReturnType<typeof request.agent>;

  beforeAll(async () => {
    agent = await authenticatedAgent();
  });

  it("accesses protected resource", async () => {
    const res = await agent.get("/api/users/me").expect(200);
    expect(res.body.email).toBe("test@test.com");
  });
});

File uploads

it("uploads an avatar", async () => {
  const res = await request(app)
    .post("/api/users/me/avatar")
    .set("Authorization", `Bearer ${token}`)
    .attach("avatar", "test/fixtures/photo.png")
    .expect(200);

  expect(res.body.avatarUrl).toBeDefined();
});

Testing error responses

it("returns 404 for missing user", async () => {
  const res = await request(app)
    .get("/api/users/nonexistent-id")
    .set("Authorization", `Bearer ${token}`)
    .expect(404);

  expect(res.body).toEqual({
    error: "NotFound",
    message: expect.stringContaining("not found"),
  });
});

it("returns 422 for invalid payload", async () => {
  const res = await request(app)
    .post("/api/users")
    .set("Authorization", `Bearer ${token}`)
    .send({ name: "" }) // missing required email
    .expect(422);

  expect(res.body.errors).toEqual(
    expect.arrayContaining([
      expect.objectContaining({ field: "email" }),
    ])
  );
});

Database setup and teardown

import { db } from "../src/db";

beforeAll(async () => {
  await db.migrate.latest();
  await db.seed.run();
});

afterAll(async () => {
  await db.destroy();
});

afterEach(async () => {
  // Roll back to a clean state between tests if using transactions
  await db.raw("ROLLBACK");
});

Best Practices

  • Always separate the Express/Koa/Fastify app from the server listener so Supertest can bind it in-process.
  • Use request.agent(app) when testing flows that rely on cookies or sessions.
  • Test both the happy path and meaningful error cases (validation, auth, not found).
  • Assert on response body structure, not just status codes — a 200 with the wrong shape is still a bug.
  • Set up and tear down test data per test or per suite to avoid ordering dependencies.
  • Use factory functions or fixtures for test data rather than relying on seed data alone.

Common Pitfalls

  • Calling app.listen() inside the app module — Supertest needs the app without a running server.
  • Forgetting await on Supertest chains — tests will pass vacuously because assertions never run.
  • Not closing database connections in afterAll, causing Jest to hang with open handles.
  • Using a shared mutable database without isolation — tests interfere with each other when run in parallel.
  • Asserting on exact object equality when the response includes generated fields like id or createdAt — use expect.objectContaining() or toMatchObject() instead.

Anti-Patterns

Over-engineering for hypothetical scale. Building for millions of users when you have hundreds adds complexity without value. Solve today's problems first.

Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide well wastes time and introduces unnecessary risk.

Premature abstraction. Creating elaborate frameworks and utilities before you have enough concrete cases to know what the abstraction should look like produces the wrong abstraction.

Neglecting error handling at boundaries. Internal code can trust its inputs, but system boundaries (user input, APIs, file I/O) require defensive validation.

Skipping documentation for obvious code. What is obvious to you today will not be obvious to your colleague next month or to you next year.

Install this skill directly: skilldb add api-testing-skills

Get CLI access →