Test Node.js HTTP APIs in-process with SuperTest — request(app) without binding a port, chained .expect assertions, auth headers, JSON body validation, and Jest integration with proper async/await patterns.
Scanned 9/5/2026
Install to Claude Code
npx -y skills add PramodDutta/qaskills --skill supertest-api --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Supertest Api?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/pramoddutta-supertest-api)More formats (shields.io, HTML) on the badges page.
---
name: SuperTest API Testing
description: Test Node.js HTTP APIs in-process with SuperTest — request(app) without binding a port, chained .expect assertions, auth headers, JSON body validation, and Jest integration with proper async/await patterns.
version: 1.0.0
author: thetestingacademy
license: MIT
tags: [supertest, api-testing, express, nodejs, jest, integration-testing, http, rest, assertions]
testingTypes: [api, integration]
frameworks: [jest, supertest, express]
languages: [typescript, javascript]
domains: [api]
agents: [claude-code, cursor, github-copilot, windsurf, codex, aider, continue, cline, zed, bolt, gemini-cli, amp]
---
# SuperTest API Testing
This skill makes an AI agent write integration tests for Express/Koa/Fastify-compatible Node HTTP apps using SuperTest: pass the app object directly to `request()` so no port is bound, chain `.expect()` for status/header checks, and assert response bodies with Jest matchers. Trigger it when a Node project exposes an Express `app`, when the user asks to test REST endpoints without spinning up a server, or when `supertest` is already in `devDependencies`.
## Core Principles
1. **Test the app object, not a running server.** `request(app)` binds to an ephemeral port per request and tears it down — no `app.listen()`, no port conflicts, no orphaned servers in CI.
2. **Export `app` separately from the listener.** The single biggest enabler: `app.ts` exports the Express app, `server.ts` calls `listen()`. Tests import `app.ts` only.
3. **Always `await` (or return) the request chain.** A SuperTest call is a thenable; forgetting `await` means the test passes before the request even fires.
4. **`.expect(status)` for transport, Jest matchers for payload.** Status codes and content-type belong in the chain; body shape belongs in `expect(res.body).toMatchObject(...)` where failure diffs are readable.
5. **Real database or none — never half-mocked.** Either run integration tests against a disposable database (Testcontainers, SQLite in-memory) or mock the data layer entirely. Mocking two of five queries gives you tests that lie.
6. **Each test owns its data.** Create the records a test needs inside the test (or a `beforeEach`), and make cleanup idempotent. Order-dependent suites rot within a sprint.
## Setup
```bash
npm install --save-dev supertest @types/supertest jest ts-jest @types/jest
```
The app/server split that makes everything testable:
```typescript
// src/app.ts
import express from 'express';
import { usersRouter } from './routes/users';
export function createApp(): express.Express {
const app = express();
app.use(express.json());
app.use('/api/users', usersRouter);
app.get('/health', (_req, res) => res.json({ status: 'ok' }));
return app;
}
```
```typescript
// src/server.ts — the ONLY file that listens; never imported by tests
import { createApp } from './app';
const port = Number(process.env.PORT ?? 3000);
createApp().listen(port, () => console.log(`listening on :${port}`));
```
First test:
```typescript
// src/app.test.ts
import request from 'supertest';
import { createApp } from './app';
const app = createApp();
describe('GET /health', () => {
it('responds 200 with status ok', async () => {
const res = await request(app)
.get('/health')
.expect('Content-Type', /json/)
.expect(200);
expect(res.body).toEqual({ status: 'ok' });
});
});
```
## Patterns
### CRUD Round-Trip with Body Assertions
```typescript
import request from 'supertest';
import { createApp } from './app';
import { resetDb } from '../test/helpers/db';
const app = createApp();
beforeEach(async () => {
await resetDb();
});
describe('POST /api/users', () => {
it('creates a user and returns 201 with the persisted record', async () => {
const res = await request(app)
.post('/api/users')
.send({ email: 'mira@example.com', name: 'Mira' })
.expect(201);
expect(res.body).toMatchObject({
email: 'mira@example.com',
name: 'Mira',
});
expect(res.body.id).toEqual(expect.any(String));
// Round-trip: the created resource is retrievable
const fetched = await request(app).get(`/api/users/${res.body.id}`).expect(200);
expect(fetched.body.email).toBe('mira@example.com');
});
it('rejects an invalid email with 400 and a field-level error', async () => {
const res = await request(app)
.post('/api/users')
.send({ email: 'not-an-email', name: 'Mira' })
.expect(400);
expect(res.body.errors).toContainEqual(
expect.objectContaining({ field: 'email' }),
);
});
});
```
### Authenticated Requests
```typescript
// test/helpers/auth.ts — log in once per suite, reuse the token
import request from 'supertest';
import type { Express } from 'express';
export async function getAuthToken(app: Express): Promise<string> {
const res = await request(app)
.post('/api/auth/login')
.send({ email: 'admin@example.com', password: 'test-password-123' })
.expect(200);
return res.body.token as string;
}
```
```typescript
import request from 'supertest';
import { createApp } from './app';
import { getAuthToken } from '../test/helpers/auth';
const app = createApp();
let token: string;
beforeAll(async () => {
token = await getAuthToken(app);
});
describe('DELETE /api/users/:id', () => {
it('returns 401 without a token', async () => {
await request(app).delete('/api/users/u_123').expect(401);
});
it('deletes with a valid bearer token', async () => {
await request(app)
.delete('/api/users/u_123')
.set('Authorization', `Bearer ${token}`)
.expect(204);
});
});
```
### Query Params, File Upload, and Custom Assertions
```typescript
// Query strings via .query() — never hand-concatenate
const res = await request(app)
.get('/api/users')
.query({ page: 2, limit: 10, sort: 'createdAt' })
.expect(200);
expect(res.body.items).toHaveLength(10);
expect(res.body.page).toBe(2);
// multipart upload
await request(app)
.post('/api/avatars')
.set('Authorization', `Bearer ${token}`)
.attach('avatar', 'test/fixtures/avatar.png')
.field('alt', 'profile picture')
.expect(201);
// Function form of .expect() for response-wide invariants
await request(app)
.get('/api/users')
.expect(200)
.expect((response) => {
if (response.body.items.some((u: { password?: string }) => u.password)) {
throw new Error('password leaked in list endpoint');
}
});
```
### Cookies and Sessions
```typescript
// Persist cookies across requests with an agent
const agent = request.agent(app);
await agent
.post('/api/auth/login')
.send({ email: 'admin@example.com', password: 'test-password-123' })
.expect(200);
// agent carries the session cookie automatically
await agent.get('/api/me').expect(200);
```
## Best Practices
- Name tests by behavior and status: `'returns 409 when email already exists'`, not `'test create user 2'`.
- Cover the unhappy paths the framework will not: malformed JSON body, missing auth, wrong content-type, oversized payload, nonexistent IDs (404 vs 400 for invalid format).
- Run integration tests serially against a shared DB (`jest --runInBand`) or give each worker its own schema; parallel workers on one mutable DB produce heisenbugs.
- Keep `Content-Type` assertions as regex (`/json/`) — servers append `; charset=utf-8`.
- Add a `jest.setup.ts` that fails tests on unhandled promise rejections; SuperTest chains silently swallow them otherwise.
- For Fastify, call `await app.ready()` before passing `app.server` to `request()`.
## Anti-Patterns
- **`app.listen()` in test setup.** Port collisions across Jest workers, orphan servers on failure. `request(app)` exists precisely so you never listen.
- **Forgetting `await` on the chain.** The test exits green while the request is in flight. Enable `@typescript-eslint/no-floating-promises` to make this a lint error.
- **Asserting entire bodies with `toEqual` including timestamps and IDs.** Use `toMatchObject` plus `expect.any(String)` for generated fields; full-body equality breaks on every schema addition.
- **One mega-test that exercises login, create, update, and delete.** When it fails at step 14 you debug all 14 steps. Split per behavior, share setup via helpers.
- **Seeding through raw SQL while testing through HTTP.** Your seed bypasses validation and hashing; create test data through the API or through the same repository layer the app uses.
- **Testing third-party middleware** (body-parser limits, cors echo) — pin your config in one test if you must, but do not re-test Express itself.
## When to Trigger This Skill
- `supertest` is in `devDependencies`, or the user asks to test Express/Koa/NestJS HTTP endpoints.
- An API has no integration tests and the user wants coverage without deploying or binding ports.
- Reviewing failing or flaky API tests that use `app.listen`, missing `await`, or shared mutable test data.
- The user asks how to test auth-protected routes, file uploads, or cookie sessions in Node.
- Setting up the app/server split so an existing Express codebase becomes testable.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!