ScreenshotNeo

BlogHow-to

Supertest: How to Test Node.js APIs

Learn how to test Node.js APIs with SuperTest: make requests, assert responses, handle async tests, and preserve cookies between requests.

By the ScreenshotNeo team4 October 20268 min read

SuperTest lets you exercise a Node.js HTTP API through its request and response boundary. Send a request to your application, then assert its status, headers, body, or a custom condition. It can bind an application to an ephemeral port when the server is not already listening, so tests usually do not need a hard-coded test port.

SuperTest supplies the HTTP request and assertion layer. Mocha, Jest, or another test runner can organize and execute the tests; no particular runner is required. The examples below use a small Express app and Node’s built-in test runner to keep the setup self-contained.

1. Export the application separately from the listener

Keep application construction in a module that tests can import. Start listening in a separate entry point so importing the app during a test does not start a production listener.

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

app.use(express.json());

app.get('/health', (req, res) => {
  res.json({ status: 'ok' });
});

app.post('/items', (req, res) => {
  const { name } = req.body;
  if (typeof name !== 'string' || name.trim() === '') {
    return res.status(400).json({ error: 'name is required' });
  }
  return res.status(201).json({ item: { name: name.trim() } });
});

module.exports = app;

// server.js
const app = require('./app');
const port = process.env.PORT || 3000;
app.listen(port, () => console.log(`Listening on ${port}`));

2. Install SuperTest and create a test

Install SuperTest as a development dependency. The repository metadata at research time listed version 7.3.0 and Node.js 14.18.0 or newer; versions change, so consult your project’s lockfile and current package metadata when selecting a version.

npm install --save-dev supertest

Create test/api.test.js. This example uses the built-in Node test runner available in modern Node.js releases; adapt the test wrapper to your existing runner if needed.

const { test } = require('node:test');
const assert = require('node:assert/strict');
const request = require('supertest');
const app = require('../app');

test('GET /health returns a successful JSON response', async () => {
  const res = await request(app)
    .get('/health')
    .expect('Content-Type', /json/)
    .expect(200);

  assert.deepEqual(res.body, { status: 'ok' });
});

test('POST /items validates input and creates an item', async () => {
  const res = await request(app)
    .post('/items')
    .send({ name: 'Notebook' })
    .expect('Content-Type', /json/)
    .expect(201);

  assert.deepEqual(res.body, { item: { name: 'Notebook' } });
});

test('POST /items rejects an empty name', async () => {
  const res = await request(app)
    .post('/items')
    .send({ name: '   ' })
    .expect(400);

  assert.equal(res.body.error, 'name is required');
});

Run the file with Node’s test runner:

node --test test/api.test.js

3. Build a SuperTest request and assert the response

The common shape is request(app).method(path). Chain expectations for status, headers, or body, then inspect the returned response for additional assertions. Assertions chained before .end() run in their declared order.

const res = await request(app)
  .get('/health')
  .expect('Content-Type', /json/)
  .expect(200);

assert.equal(res.body.status, 'ok');

Use a regular expression for content types because a response may include a charset parameter. The body is parsed when the response content type indicates a supported structured format; check the response headers if you need to diagnose unexpected parsing.

4. Test POST requests, headers, and custom conditions

Use .send() to provide a request body. Set headers with .set(). SuperTest expectations can check status, header values, or a response body value. For application-specific checks, assert on the returned response in the test runner’s normal assertion library.

test('creates an item with an API key header', async () => {
  const res = await request(app)
    .post('/items')
    .set('Authorization', 'Bearer test-token')
    .set('Accept', 'application/json')
    .send({ name: 'Notebook' })
    .expect('Content-Type', /json/)
    .expect(201);

  assert.equal(res.body.item.name, 'Notebook');
});

This example demonstrates request construction only; the sample route does not validate the authorization header. Add assertions that match the behavior your own middleware and route actually implement. For an authentication test, use a test credential or an intentionally invalid credential and assert the API’s documented response.

5. Choose a completion style that fits your test runner

Async/await

Await the request chain and let a rejected promise fail the test. This style avoids manually signaling completion.

test('returns health status', async () => {
  const res = await request(app).get('/health').expect(200);
  assert.equal(res.body.status, 'ok');
});

Promise chaining

Return the promise from the test so the runner waits for the request and sees a rejection.

test('returns health status with a promise', () => {
  return request(app)
    .get('/health')
    .expect(200)
    .then((res) => {
      assert.equal(res.body.status, 'ok');
    });
});

Callback with .end()

If using .end(), pass its error to the runner’s failure path. A failed SuperTest expectation is delivered as an error to this callback.

test('returns health status with a callback', (t, done) => {
  request(app)
    .get('/health')
    .expect(200)
    .end((err, res) => {
      if (err) return done(err);
      try {
        assert.equal(res.body.status, 'ok');
        done();
      } catch (assertionError) {
        done(assertionError);
      }
    });
});

In runners that accept a completion callback directly in an expectation, follow that runner’s callback convention. Do not combine a returned promise with a callback completion signal for the same test; choose one completion path so the runner has a single clear result.

6. Preserve cookies across requests with an agent

Independent calls made with request(app) should be treated as separate requests. Use request.agent(app) when a sequence needs state such as cookies to persist.

const agent = request.agent(app);

// Example assumes the application has these routes:
// GET /session sets a session cookie.
// GET /account requires that cookie.
test('keeps a cookie between requests', async () => {
  await agent.get('/session').expect(200);
  const res = await agent.get('/account').expect(200);
  assert.equal(res.body.authenticated, true);
});

The route behavior above is an example contract; implement it in your app or substitute your own session-establishing and session-protected paths. Keep test users and backing data isolated according to the application’s own setup. SuperTest does not define a universal database cleanup strategy.

7. Other useful request options

Need Approach
Send JSON Call .send({ key: 'value' }) and assert the response content type and status.
Set request headers Call .set('Header-Name', 'value') before sending.
Check status or response data Chain .expect(...), then use the returned response for custom assertions.
Keep cookie state Reuse a request.agent(app) instance across the requests in the flow.
Use an existing server Pass an HTTP server or application function to request(). If the server is already listening, SuperTest can use it; otherwise it binds an ephemeral port.
Exercise HTTP/2 The official README documents an HTTP/2 option. Use it only when the server and project requirements call for HTTP/2, and check the current README for its exact setup.

For ordinary route tests, passing the application is usually enough. It avoids coordination around a fixed port and lets SuperTest manage the local test server lifecycle.

8. Troubleshooting common failures

Symptom Likely cause Fix
The test exits before the request finishes The test did not return or await the request promise, or the callback style did not signal completion. Return the chain, use async/await, or call the runner’s completion callback from .end().
An assertion fails but the callback test passes The .end() callback ignored the error. Forward err to the test runner, for example done(err), before checking the response.
Response body is empty or not an object The route returned a different content type or body shape than expected. Inspect status and headers, ensure the route sends JSON when JSON is expected, and assert the actual contract.
A protected route returns an authorization error The request did not include the required header or the test token is invalid for the app’s test configuration. Set the header the middleware expects and use a test credential configured for that environment.
A second request behaves as if logged out Separate request objects do not share agent cookie state. Create one request.agent(app) and use it for both session setup and the follow-up request.
Address already in use The test starts a fixed-port listener that conflicts with another process or test. Import the app and let SuperTest bind it, or use an existing server deliberately managed by the test suite.
Tests interfere through shared records Requests are touching shared application data or sessions. Give each test isolated inputs and apply cleanup appropriate to the app’s persistence layer.

9. Performance, reliability, and cost

SuperTest exercises the application’s HTTP boundary, so it can cover routing, middleware, serialization, and response behavior together. It is not a substitute for tests that isolate individual functions, and the request alone cannot establish whether external services or persistent data are configured correctly.

Keep tests reliable by importing the app without starting an unrelated listener, asserting the API’s observable contract, and isolating stateful data between cases. Avoid unsourced timing expectations; performance depends on the application and its dependencies. SuperTest is an npm development dependency, so account for its package in the project’s dependency and lockfile management. No benchmark or universal runtime cost is established by the sources used here.

10. Or skip the browser setup

SuperTest is for exercising your own Node.js API. If your workflow also needs screenshots of web pages, ScreenshotNeo provides a website screenshot API and MCP server. It accepts one GET request and returns an image or PDF. Its API options include full-page capture, element capture, viewport and device settings, custom CSS or JavaScript, waits, request blocking, caching, async jobs, and bulk capture.

For example, request a screenshot of a public page with cURL:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

See the ScreenshotNeo API documentation for the request parameters. Its cookie/consent handling accepts banners like a visitor and removes 60+ known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits cost nothing, and response headers identify the page verdict and billing status. An MCP server gives AI agents tools to take screenshots, inspect page information, and capture PDFs. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots. Sign up free and start with 1,000 screenshots a month, no card required.

FAQ

Do I need Express to use SuperTest?

No. SuperTest accepts an HTTP server or an application function. Express is used here only to make the example concrete.

Does SuperTest require Jest or Mocha?

No. It provides the request and assertion workflow; a test runner is optional for organizing and executing a suite. The official examples also show use without a test framework.

Can I test an API that is already running?

Yes. SuperTest can receive an HTTP server. For app-focused tests, passing the application lets it bind an ephemeral port when needed.

Reuse a request.agent(app) instance for the login or session-establishing request and the later protected request.