supertest review
supertest 7.2.2 drives a Node HTTP server through SuperAgent and adds chainable assertions for status, headers, bodies, and cookies. Pass an Express-style app function or an `http.Server`; if it is not listening, supertest binds an ephemeral port for the request. It also accepts a real base URL for black-box checks, supports persistent cookie agents, multipart uploads, redirects, authentication, and HTTP/2. Version 7.2 introduced cookie assertion helpers, then 7.2.1 fixed a case-sensitive module path and 7.2.2 replaced the cookie helper's `should` dependency with native assertions.
supertest 7.2.2 installed in 1.9 seconds and occupied 6 MB across 44 packages in our sandbox with 0 audit findings, but it supplied no TypeScript types and could not build for browsers. Install it for Node HTTP boundary tests; skip it for browser execution, lightweight handler injection, or typed tests that cannot accept DefinitelyTyped lag.
We installed it
| Install | ✓ · 1.9s | 44 packages on disk · 6 MB |
| Import | ✓ | ESM import works · require() works · CommonJS package |
| Browser | n/a | could not be bundled for the browser (Node-only code, most likely) |
| Types | — | no TypeScript types found |
| Known vulns | 0 | 0 critical · 0 high · 0 moderate · 0 low (npm audit) |
Answers from our run
Does supertest install cleanly?
Yes. In a fresh container with an empty cache, npm install supertest finished in 2 seconds, leaving 44 packages and 6 MB on disk. npm audit reported no known vulnerabilities.
Can supertest run in a browser?
Not directly: esbuild could not bundle it for the browser in our run, which normally means it depends on Node built-ins. Use it on the server, or find a browser-targeted alternative.
Does supertest work with both ESM and CommonJS?
Yes. Both import 'supertest' and require('supertest') worked in Node 22 in our run. The package is published as CommonJS.
Does supertest include TypeScript types?
No type declarations were found in our install, so TypeScript users need their own declarations.
supertest or light-my-request: which should you use?
light-my-request: Use it for fast in-process injection against Fastify-style handlers when a real socket is unnecessary. supertest 7.2.2 installed in 1.9 seconds and occupied 6 MB across 44 packages in our sandbox with 0 audit findings, but it supplied no TypeScript types and could not build for browsers.
When should you not use supertest?
Tests run in a browser, edge worker, or service worker. Our browser bundle failed because supertest depends on Node server and networking APIs.
Use it if
- Node API tests should exercise the real HTTP boundary without managing a fixed test port.
- Status, JSON, header, redirect, cookie, multipart, or authentication behavior belongs in one readable request chain.
- A login flow needs a cookie jar shared across several requests through `request.agent(app)`.
- The same test style should cover an in-process app in unit runs and a deployed HTTP URL in smoke tests.
- Tests run in a browser, edge worker, or service worker. Our browser bundle failed because supertest depends on Node server and networking APIs.
- A TypeScript project requires first-party declarations. Version 7.2.2 ships no types, so it relies on the separately maintained `@types/supertest` package.
- The framework has a faster injection API and network fidelity is unnecessary. Fastify's `light-my-request` avoids opening a socket for handler-level tests.
- You only need standards-based outbound HTTP calls. Node's `fetch` or Undici has less assertion-specific machinery and no test-server lifecycle behavior.
- Your tests assume every 4xx or 5xx response is a normal resolved result without declaring its status. SuperAgent treats unexpected error statuses as errors, which can obscure the intended assertion.
Setup reality
We installed supertest 7.2.2 in a fresh Node 22 Bookworm sandbox in 1.9 seconds. The install left 44 packages and used 6 MB on disk. npm audit reported 0 known vulnerabilities at every severity. The package itself declares 3 direct dependencies, 0 peer dependencies, and 76 KB unpacked under MIT. It is CommonJS with no exports map; require() and ESM import both worked. No TypeScript declarations were found.
There are no credentials or config files for in-process tests. Pass the app callback to request(app) and supertest opens an ephemeral listener if needed. That still uses a real local socket, so close database clients, queues, timers, and any server you started yourself in test teardown. A base URL tests a live service instead; credentials, seed data, and cleanup then belong to that environment and should never be assumed isolated.
A chain runs when awaited, returned, given a callback, or ended with .end(). With .end(), failed expectations arrive as err; the callback must pass that error to the test runner. Promise and async forms are harder to mishandle. Add .expect(404) or the intended error status, because an undeclared non-2xx response is treated as an error by the SuperAgent layer. Expectation callbacks execute in registration order and may normalize the response before later assertions.
Cookie state survives only through request.agent(), not separate request(app) calls. Multipart fixtures are read from the filesystem, and relative paths depend on the process working directory. HTTP/2 is opt-in with { http2: true }. The package is Node-only: our esbuild browser target could not resolve its server-oriented code. TypeScript users need @types/supertest, and those community types can trail newly added helpers such as the version 7.2 cookie assertions.
Patterns
Check status and JSON together assert-json-response
const request = require('supertest');
const app = require('../app');
it('returns one user', async () => {
await request(app)
.get('/user')
.expect('Content-Type', /json/)
.expect(200, { name: 'john' });
});An object body uses deep equality after parsing, and the 2 expectations run in the order shown.
Await and inspect selected fields inspect-response
const response = await request(app)
.get('/users')
.set('Accept', 'application/json')
.expect(200);
expect(response.body).toEqual([{ id: 1 }]);Awaiting starts the request; declaring the 200 status makes an unexpected error response fail at the HTTP boundary.
Send a JSON request body post-json
await request(app)
.post('/users')
.send({ name: 'john' })
.set('Accept', 'application/json')
.expect(201);Passing an object to `.send()` selects JSON serialization; a pre-encoded string follows different content-type handling.
Declare an expected 404 test-error-response
const response = await request(app)
.get('/users/missing')
.expect('Content-Type', /json/)
.expect(404);
expect(response.body.code).toBe('USER_NOT_FOUND');SuperAgent treats an undeclared 4xx response as an error, so put the expected 404 in the chain.
Reuse cookies after login persist-login-cookie
const agent = request.agent(app);
await agent
.post('/login')
.send({ username: 'ann', password: 'secret' })
.expect(204);
await agent.get('/dashboard').expect(200);Use the same 1 agent for the flow; separate `request(app)` calls create separate cookie jars.
Send a bearer token set-authorization
await request(app)
.get('/me')
.set('Authorization', `Bearer ${token}`)
.expect(200);Setting the header directly works even when the separately published TypeScript declarations lag a SuperAgent auth helper.
Check a redirect without following it assert-redirect
await request(app)
.get('/old-path')
.expect(302)
.expect('Location', '/new-path');Redirect following is off by default; use `.redirects(1)` only when the final 200 response is the behavior under test.
Upload fields and a fixture upload-multipart
const path = require('node:path');
await request(app)
.post('/avatar')
.field('name', 'profile image')
.attach('avatar', path.join(__dirname, 'fixtures', 'avatar.png'))
.expect(201);An absolute fixture path keeps the test stable when 1 runner changes the process working directory.
Validate a response shape write-custom-assertion
function hasPageLinks(response) {
if (!('next' in response.body)) throw new Error('missing next');
if (!('previous' in response.body)) throw new Error('missing previous');
}
await request(app)
.get('/items')
.expect(200)
.expect(hasPageLinks);A custom expectation fails by throwing; its return value is ignored by the assertion chain.
Remove volatile fields before comparison normalize-before-assertion
await request(app)
.post('/users')
.send({ name: 'John' })
.expect((response) => {
delete response.body.createdAt;
response.body.name = response.body.name.toLowerCase();
})
.expect(201, { id: 1, name: 'john' });Expectations execute in order, so the first function changes the body seen by the later exact assertion.
Exercise an HTTP/2 app enable-http2
await request(app, { http2: true })
.get('/health')
.expect(200, { ok: true });HTTP/2 is opt-in through the second argument; the default path uses ordinary Node HTTP/1.1.
Return callback failures to Mocha propagate-callback-error
it('creates a user', (done) => {
request(app)
.post('/users')
.send({ name: 'john' })
.expect(201)
.end((error) => done(error));
});`.end()` sends failed expectations through its first argument; dropping that 1 error can make a broken test pass or time out.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| light-my-request | npm | Use it for fast in-process injection against Fastify-style handlers when a real socket is unnecessary. |
| undici | npm | Use it for standards-based HTTP calls to an already running service and bring assertions from the test runner. |
| chai-http | npm | Use it when Chai assertions are already the suite's central style and its request plugin fits the runner. |
| axios | npm | Use it for general HTTP client calls when app binding, cookie agents, and response assertion chains are unnecessary. |
More testing guides
pytest · chai · vitest · jsdom · playwright · coverage · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

