Skip to main content
Back to Blog
API Testing
2026-05-18

SuperTest Node.js API Testing Complete Guide 2026

Complete guide to SuperTest for Node.js API testing. Setup with Jest, Express, Fastify, authentication, JSON validation, mocking, and CI integration patterns.

SuperTest Node.js API Testing Complete Guide 2026

SuperTest is the de facto standard library for HTTP integration testing in the Node.js ecosystem. Built on top of superagent, it lets you write fluent, expressive tests against any Node.js HTTP server - Express, Fastify, NestJS, Koa, or a plain http.createServer instance. Rather than spinning up the server on a network port and making external HTTP calls, SuperTest can drive your app directly in the same process, eliminating network flakiness and making tests blazingly fast. Combined with Jest or Mocha and a JSON Schema validator, SuperTest forms the backbone of robust API test suites for thousands of Node.js teams.

This complete guide covers every aspect of SuperTest in 2026: installation alongside Jest, Express and Fastify integration, request and response patterns, authentication (basic, bearer, OAuth, cookies), JSON validation, file uploads, contract testing with OpenAPI, mocking external services, parallel execution, and CI integration. Real code examples cover a full SaaS API test suite. By the end you'll be ready to write production-grade API tests for your Node.js services.

Key Takeaways

  • SuperTest drives Node HTTP servers in-process - no network calls
  • Works with Express, Fastify, NestJS, Koa, plain http
  • Pairs naturally with Jest, Mocha, Vitest
  • Fluent .post(), .get(), .send(), .expect() chains
  • Handles cookies, redirects, and auth automatically
  • Built-in assertion shortcuts for status, headers, body
  • Faster than tests that hit a real network port

Installation

npm install --save-dev supertest @types/supertest jest

Basic Test With Express

// app.js
const express = require('express');
const app = express();
app.use(express.json());

app.get('/users/:id', (req, res) => {
  res.json({ id: req.params.id, name: 'Alice' });
});

module.exports = app;
// app.test.js
const request = require('supertest');
const app = require('./app');

describe('GET /users/:id', () => {
  it('returns user data', async () => {
    const response = await request(app).get('/users/42');
    expect(response.status).toBe(200);
    expect(response.body).toEqual({ id: '42', name: 'Alice' });
  });
});

Notice we pass app directly to request() - no need to listen on a port.

Fluent Chains

it('chained assertions', async () => {
  await request(app)
    .post('/users')
    .send({ name: 'Bob', email: 'bob@example.com' })
    .set('Content-Type', 'application/json')
    .expect(201)
    .expect('Content-Type', /json/)
    .expect((res) => {
      expect(res.body.id).toBeDefined();
      expect(res.body.name).toBe('Bob');
    });
});

HTTP Methods

await request(app).get('/users');
await request(app).post('/users').send({ name: 'Alice' });
await request(app).put('/users/1').send({ name: 'Alice Updated' });
await request(app).patch('/users/1').send({ name: 'A' });
await request(app).delete('/users/1');

Query Parameters

await request(app)
  .get('/users')
  .query({ limit: 10, sort: 'name' })
  .expect(200);

Headers

await request(app)
  .get('/me')
  .set('Authorization', 'Bearer ' + token)
  .set('X-API-Key', 'secret')
  .expect(200);

Authentication

Basic Auth

await request(app)
  .get('/admin')
  .auth('user', 'pass')
  .expect(200);

Bearer Token

async function loginAndGetToken() {
  const response = await request(app)
    .post('/auth/login')
    .send({ email: 'test@example.com', password: 'secret' });
  return response.body.access_token;
}

it('uses bearer token', async () => {
  const token = await loginAndGetToken();
  await request(app)
    .get('/me')
    .set('Authorization', `Bearer ${token}`)
    .expect(200);
});

Cookies

const agent = request.agent(app);

await agent
  .post('/login')
  .send({ email: 'test@example.com', password: 'secret' })
  .expect(200);

await agent
  .get('/dashboard')
  .expect(200);

The agent persists cookies across requests.

File Upload

await request(app)
  .post('/uploads')
  .attach('file', '/tmp/test.csv')
  .field('description', 'test data')
  .expect(201);

JSON Schema Validation

npm install --save-dev ajv ajv-formats
const Ajv = require('ajv');
const addFormats = require('ajv-formats');

const ajv = new Ajv();
addFormats(ajv);

const userSchema = {
  type: 'object',
  required: ['id', 'name', 'email'],
  properties: {
    id: { type: 'string' },
    name: { type: 'string' },
    email: { type: 'string', format: 'email' },
  },
};

it('response matches schema', async () => {
  const response = await request(app).get('/users/1').expect(200);
  const validate = ajv.compile(userSchema);
  expect(validate(response.body)).toBe(true);
});

Setup And Teardown

const request = require('supertest');
const app = require('./app');
const { sequelize } = require('./db');

beforeAll(async () => {
  await sequelize.sync({ force: true });
});

beforeEach(async () => {
  await sequelize.truncate({ cascade: true });
});

afterAll(async () => {
  await sequelize.close();
});

describe('Users API', () => {
  it('creates user', async () => {
    const res = await request(app)
      .post('/users')
      .send({ name: 'Alice', email: 'alice@example.com' });
    expect(res.status).toBe(201);
  });
});

Mocking External Services

Combined with nock:

npm install --save-dev nock
const nock = require('nock');

beforeEach(() => {
  nock('https://payment.example.com')
    .post('/charge')
    .reply(200, { status: 'paid' });
});

afterEach(() => nock.cleanAll());

it('charges card via external service', async () => {
  const res = await request(app)
    .post('/orders/1/pay')
    .send({ card: '4242' });
  expect(res.status).toBe(200);
});

Fastify

// fastify-app.js
const fastify = require('fastify')();

fastify.get('/users/:id', async (req) => {
  return { id: req.params.id, name: 'Alice' };
});

module.exports = fastify;
// test.js
const request = require('supertest');
const fastify = require('./fastify-app');

beforeAll(async () => fastify.ready());
afterAll(async () => fastify.close());

it('gets user', async () => {
  const res = await request(fastify.server).get('/users/1');
  expect(res.status).toBe(200);
});

NestJS

import { Test } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './app.module';

describe('Users (e2e)', () => {
  let app: INestApplication;

  beforeAll(async () => {
    const moduleFixture = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();
    app = moduleFixture.createNestApplication();
    await app.init();
  });

  afterAll(async () => app.close());

  it('GET /users', () => {
    return request(app.getHttpServer())
      .get('/users')
      .expect(200);
  });
});

Contract Testing With OpenAPI

npm install --save-dev jest-openapi
const jestOpenAPI = require('jest-openapi').default;
const path = require('path');

jestOpenAPI(path.resolve(__dirname, './openapi.yaml'));

it('response satisfies OpenAPI spec', async () => {
  const res = await request(app).get('/users/1');
  expect(res).toSatisfyApiSpec();
});

This checks the response against the documented OpenAPI schema.

Performance Patterns

PatternBenefit
Pass app directly (no .listen)No port allocation
One beforeAll per fileAvoid repeated init
Use agent for cookie flowsImplicit auth
Mock external servicesDeterministic + fast
Parallel test filesJest runs in workers

Parallel Execution

Jest runs test files in parallel by default. To control:

{
  "jest": {
    "maxWorkers": "50%"
  }
}

For tests that share state, use --runInBand:

jest --runInBand

Real Suite Example

const request = require('supertest');
const app = require('../app');
const { sequelize, User } = require('../db');

describe('Users API', () => {
  let token;

  beforeAll(async () => {
    await sequelize.sync({ force: true });
    await User.create({
      email: 'test@example.com',
      passwordHash: await bcrypt.hash('secret', 10),
    });
    const res = await request(app).post('/auth/login').send({
      email: 'test@example.com',
      password: 'secret',
    });
    token = res.body.access_token;
  });

  afterAll(async () => sequelize.close());

  describe('GET /users', () => {
    it('returns 401 without auth', async () => {
      const res = await request(app).get('/users');
      expect(res.status).toBe(401);
    });

    it('returns user list with auth', async () => {
      const res = await request(app)
        .get('/users')
        .set('Authorization', `Bearer ${token}`);
      expect(res.status).toBe(200);
      expect(Array.isArray(res.body)).toBe(true);
    });
  });

  describe('POST /users', () => {
    it('creates user with valid data', async () => {
      const res = await request(app)
        .post('/users')
        .set('Authorization', `Bearer ${token}`)
        .send({ name: 'Alice', email: 'alice@example.com' });
      expect(res.status).toBe(201);
      expect(res.body.id).toBeDefined();
    });

    it('returns 400 on invalid email', async () => {
      const res = await request(app)
        .post('/users')
        .set('Authorization', `Bearer ${token}`)
        .send({ name: 'Alice', email: 'invalid' });
      expect(res.status).toBe(400);
    });

    it('returns 409 on duplicate email', async () => {
      const res = await request(app)
        .post('/users')
        .set('Authorization', `Bearer ${token}`)
        .send({ name: 'Test', email: 'test@example.com' });
      expect(res.status).toBe(409);
    });
  });
});

CI Integration

name: API Tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - run: npm test -- --coverage
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: coverage
          path: coverage/

Anti-Patterns

Anti-PatternBetter
Spinning up real serverPass app directly
Hitting external APIsMock with nock
Shared test dataPer-test fixtures
No cleanupbeforeEach truncate
Hardcoded base URLPass app, no URL needed

Comparison

ToolSpeedSetupBest For
SuperTestFastestEasyExpress/Fastify in-process
Axios + JestMediumEasyExternal API tests
Postman/NewmanSlowManualSmoke + manual reuse
RestAssured (Java)MediumMore setupJava backends

Debugging

When a test fails:

it('debugging example', async () => {
  const res = await request(app).get('/users/42');
  console.log('Status:', res.status);
  console.log('Body:', JSON.stringify(res.body, null, 2));
  console.log('Headers:', res.headers);
  expect(res.status).toBe(200);
});

Or use Jest --verbose mode.

Conclusion

SuperTest is the right default for Node.js API testing. Its in-process model eliminates network flakiness, the fluent chain syntax is concise, and the ecosystem support is excellent across Express, Fastify, NestJS, and Koa. Combined with Jest, a JSON Schema validator, and nock for mocking, you have a complete API testing toolkit in a few npm packages.

Start by adding a single SuperTest file to your project. Test one endpoint with multiple scenarios - happy path, error cases, edge cases. Layer in shared setup, schema validation, and mocks. Within a sprint you'll have a fast, reliable API test suite. Explore our skills directory or the API testing complete guide for broader patterns.

SuperTest Node.js API Testing Complete Guide 2026 | QASkills.sh