How to Test REST APIs With Supertest and Node.js
Nandini Pawar
Posted On: August 15, 2025
14 Min
Supertest is a Node.js library used to automate API testing by sending HTTP requests and asserting responses. Built on SuperAgent, it integrates smoothly with Express and test runners like Jest or Mocha. You can use it to validate APIs, catch regressions, and ensure backend reliability before code reaches production.
Overview
Supertest is a library written in Node.js used for testing HTTP APIs. It extends the SuperAgent HTTP client with a fluent API that allows developers and QA engineers to send requests and make assertions on responses.
Features of Supertest
- HTTP Method Support: Send GET, POST, PUT, DELETE, and other HTTP requests.
- Fluent Assertions: Chain .expect() statements for status codes, headers, and response body.
- No Port Binding Needed: Test Express apps or http.Server instances without manually starting the server.
- Integration Ready: Works seamlessly with Jest, Mocha, Jasmine, and other JavaScript test runners.
- Custom Validation: Use inline callbacks to apply custom logic to the response.
- Asynchronous Support: Write tests using async/await or Promises for clean, readable code.
How to Test REST APIs with Supertest
- Install: Set up Supertest with a test runner like Jest using npm install –save-dev supertest jest.
- Import: Bring in Supertest and your Express or Node.js app into the test file.
- Write Tests: Use .get(), .post(), and .expect() to send requests and check responses.
- Send Data: Use .send() for payloads and .set() for custom headers.
- Use Async/Await: Keep your test code clean and readable with async functions.
- Cover Edge Cases: Include tests for 4xx/5xx responses and validation errors.
- Run in CI: Execute tests using npm test or jest as part of your build pipeline.
TABLE OF CONTENTS
What Is Supertest?
Supertest is a lightweight Node.js library built for testing HTTP APIs directly and efficiently. It wraps the SuperAgent HTTP client with a fluent, chainable API that’s perfect for integration and end-to-end testing of backend services.
You can pass an http.Server or Express app directly, no need to manage listening ports, as Supertest handles it internally. It works seamlessly with popular JavaScript testing frameworks like Jest, Mocha, and Jasmine, supporting both async/await and promise-based syntax.
Key Features of Supertest
Below are the standout features that make Supertest a helpful library for QA engineers:
- Chainable HTTP Assertions: Supertest allows chained assertions in a readable style. For example:
- Full Control Over All HTTP Methods: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, you’re not limited. Support for .send(), .auth(), .query(), .field() and even multipart attach() calls is available.
- No External Server Required: By passing your express() app (or any Node.js HTTP server) to Supertest, it handles the server startup internally. There’s no need to configure ports or add separate server scripts for testing.
- Test Framework Agnostic: Works with Jest, Mocha, Jasmine, AVA, whatever your project uses. You can mix chainable .then() or async/await syntax (favored in Jest), or classic callback pattern via .end(done).
- Comprehensive Response Validation: Supertest offers built-in support to assert status codes, headers, JSON fields, and custom logic using inline .expect() callbacks. You can also transform or validate response data dynamically within these handlers for more flexible assertions.
- Authentication & Session Support: Via .auth(username, password) or header-based tokens, you can add auth checks easily. Agents also support cookies and persistent sessions across calls using the request.agent(app).
- TypeScript Friendly: A TypeScript definition package (@types/supertest) is available and maintained. If you write tests in TS, expect full type inference and IDE completion.
1 2 3 4 5 |
request(app) .get('/api/users') .set('Accept', 'application/json') .expect('Content-Type', /json/) .expect(200) |
Fields, bodies, response headers, and custom checks can all be validated inline.

Run your automated tests across 3000+ real browsers. Try LambdaTest Today!
Set Up Supertest
You’ll need Supertest for making requests and a test runner like Jest or Mocha.
Let’s use Jest.
1 |
npm install supertest jest --save-dev |
Make sure your Express (or other framework) app is exported without starting the server directly in the same file.
Structure Your App for Testing
Make sure your Express (or other framework) app is exported without starting the server directly in the same file.
For example:
1 2 3 4 5 6 7 8 9 10 |
const express = require('express'); const app = express(); app.use(express.json()); app.get('/api/hello', (req, res) => { res.json({ message: 'Hello, World!' }); }); module.exports = app; |
1 2 3 4 5 6 |
const app = require('./app'); const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); }); |
This separation allows Supertest to import the app directly without binding to a port.
Write Tests With Supertest
With Jest, create a test file:
1 2 3 4 5 6 7 8 9 10 |
const request = require('supertest'); const app = require('./app'); describe('GET /api/hello', () => { it('should return a hello message', async () => { const res = await request(app).get('/api/hello'); expect(res.statusCode).toBe(200); expect(res.body).toHaveProperty('message', 'Hello, World!'); }); }); |
Add a Jest test script in the package.json file:
1 2 3 |
"scripts": { "test": "jest" } |
Then run the below command to execute your tests:
1 |
npm test |
Advanced Use Cases of Supertest
Supertest is often introduced as a quick way to test HTTP endpoints in Node.js, but its capabilities extend far beyond basic “send a request and check a status code” scenarios.
Here are some advanced use cases worth knowing about:
Testing With Complex Authentication Flows
Supertest can handle multi-step login processes, token-based auth, and cookie sessions. You can store authentication tokens or cookies from one request and reuse them in subsequent requests.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
let token; beforeAll(async () => { const res = await request(app) .post('/login') .send({ username: 'user', password: 'pass' }); token = res.body.token; }); test('GET /profile with JWT', async () => { await request(app) .get('/profile') .set('Authorization', `Bearer ${token}`) .expect(200); }); |
This approach allows you to mimic a real client session in your tests.
Chaining Requests for Stateful Scenarios
If your API has workflows (e.g., create -> update -> delete), you can chain requests in a single test to verify data persistence and state changes.
1 2 3 4 5 6 7 |
test('Full CRUD flow', async () => { const createRes = await request(app).post('/items').send({ name: 'test' }); const id = createRes.body.id; await request(app).put(`/items/${id}`).send({ name: 'updated' }).expect(200); await request(app).delete(`/items/${id}`).expect(204); }); |
This helps catch integration issues that unit tests might miss.
Testing File Uploads and Downloads
Supertest can simulate multipart/form-data uploads and verify binary responses.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
test('Upload and download file', async () => { await request(app) .post('/upload') .attach('file', 'tests/fixtures/sample.txt') .expect(200); const res = await request(app) .get('/download/sample.txt') .expect('Content-Type', /text\/plain/) .expect(200); expect(res.text).toContain('Expected content'); }); |
This is especially useful for APIs handling images, CSVs, or documents.
Validating Response Schemas
Integrating Supertest with schema validation libraries like Joi or ajv lets you check that responses match your API contract, preventing silent breaking changes.
1 2 3 4 5 6 7 8 9 10 11 12 |
const Joi = require('joi'); const userSchema = Joi.object({ id: Joi.number().required(), name: Joi.string().required(), }); test('GET /user/:id returns valid schema', async () => { const res = await request(app).get('/user/1').expect(200); const { error } = userSchema.validate(res.body); expect(error).toBeUndefined(); }); |
Best Practices for Using Supertest
QA engineers can apply several well-tested strategies to keep their API suite robust and maintainable:
- Validate Response Bodies, Not Just Status: Checking only for status codes (e.g., 200 OK) is “smoke testing” and that’s not enough. Always validate the schema, data types, and key fields based on your API contract.
- Use Fixtures and Factory Data: Avoid hard-coding user IDs or test data. Use factory functions or JSON fixtures so tests can generate predictable but randomized payloads (e.g., userFactory()). This makes tests self-contained and easier to regenerate when requirements change.
- Isolate Test Environment: Ensure the database or in-memory store resets between test runs. For integration, use ephemeral test DBs or in-memory mocks so tests don’t bleed into each other. Cleanup logic belongs in afterEach() or afterAll() hooks.
- Avoid Test Fragility: Keep assertions specific, not brittle. Instead of checking full-body stringification, assert specific keys or use regex-based matches. For date fields or ephemeral IDs, use. expect(res => { expect(typeof res.body.createdAt).toBe(‘string’) }) instead of exact match.
- Keep Test Suites Lean: Group related tests; use shared beforeAll() for authentication tokens. Avoid long, sequential test files, modularize into topic-based files (e.g. login.test.js, products.test.js). This improves readability and maintenance for QA teams.
- Test Both Positive & Negative Scenarios: Don’t limit tests to happy paths, include scenarios like 4xx errors, missing fields, auth failures, and rate limits. Covering these edge cases ensures robust API behavior and real-world reliability.
Common Pitfalls and How to Avoid Them
Mistakes are the bane of test suites. Here are frequent pitfall scenarios to watch out for:
- Only Checking HTTP Status (Shallow Testing): As mentioned, testing only .expect(200) reveals very little. API responses often contain crucial data that must be verified. Supertest makes it easy to add body assertions, use them.
- Missing Error Case Coverage: Tests that focus exclusively on valid input ignore real failure paths. What happens if your API receives invalid JSON, duplicate keys, or missing headers? These should produce controlled error payloads and proper HTTP response codes.
- Flaky or Slow Tests Due to External APIs: If your API calls a third-party service, avoid hitting it directly. This makes tests fail when the third-party is slow or down. Use stub tools (nock, msw) to decouple and speed up testing.
- Race Conditions in Async Tests: When not using proper await or callback done(), tests may exit before assertions run. Use consistent async patterns. If mixing callback-style .end(), make sure to call done(err) properly, or else tests may quietly succeed or hang.
- Port Collisions and Shared State: Multiple Supertest instances can conflict if your server uses static ports or global state. Use isolated servers per test (by re-requiring app()), reset memory stores, or run tests serially for stateful endpoints.
Supertest vs Other Testing Tools
There are plenty of options available, so let’s see how Supertest stacks up against some well-known tools:
Tool | Type | Best For | Notes |
---|---|---|---|
Supertest | Code-based (Node.js) | Automation with full control, CI-ready | Requires JS expertise. No GUI. |
Postman/Newman | GUI + CLI | Manual exploration, exportable automation | Great for non-dev stakeholders. Less scalable for versioned suites. |
Insomnia | GUI + CLI | Building requests, team sharing | Similar to Postman. GUI-heavy. |
Chakram | Code-based (JavaScript) | Fluent assertions, BDD style | No longer actively maintained. Less community support. |
Frisby.js | Code-based (JavaScript) | JSON-focused API tests using Jest style | Built around fetch, dev momentum slower. |
PactumJS | Code-based (Node.js) | Contract testing + assert flows | More recent project with DSL-based syntax. |
Pro-tip: If you’re working with REST APIs in Node.js, you’ve probably used Supertest to write detailed tests. It gives you full control over endpoints, assertions, and works seamlessly with test frameworks like Mocha or Jest.
That said, there’s also a growing category of GenAI testing tools like LambdaTest KaneAI that you might find useful if you’re looking to reduce the manual effort of writing API tests. These tools are designed to simplify API test creation by letting you write in natural language commands.
LambdaTest KaneAI is a Generative AI testing tool that allows you to plan, author, debug, and evolve end-to-end tests using natural language commands, eliminating much of the manual coding overhead typically required for test automation.
To get started, check out this guide on API testing with KaneAI.
Conclusion
Supertest brings speed, simplicity, and precision to API testing in Node.js. From setup to advanced usage, it streamlines how QA engineers validate endpoints, handle edge cases, and ensure consistent backend behavior.
With built-in assertion tools, flexible integration with test runners, and best practices for scalable test design, Supertest stands out as a reliable choice for service-level testing. Compared to other tools, it offers a lean, expressive approach without compromising depth.
Frequently Asked Questions (FAQs)
What is Supertest and why should I use it?
Supertest is a lightweight Node.js library for testing HTTP APIs. It wraps SuperAgent and allows developers and QA engineers to write fluent, readable HTTP assertions for backend services. It’s ideal for integration and service-level testing without UI involvement.
Can I test an Express app without starting a server or binding ports?
Yes, you can pass your Express app or http.Server instance directly to the request(app). Supertest automatically binds to an ephemeral port, so there’s no need to call .listen() or manually manage ports.
Which HTTP methods does Supertest support?
Supertest supports all standard HTTP methods, including GET, POST, PUT, DELETE, PATCH, HEAD, and OPTIONS. You can also simulate complex payloads, multipart uploads, and authorization flows.
How do I assert status codes, headers, or custom logic?
You can chain multiple .expect() calls like .expect(200), .expect(‘Content-Type’, /json/), or use inline functions to validate response data dynamically with .expect(res => { /* logic */ }).
Is Supertest compatible with Jest or Mocha?
Yes, Supertest integrates well with popular JavaScript test runners like Jest, Mocha, and Jasmine. It supports async/await and Promises, enabling cleaner and more modern test syntax.
How do I mock external API calls in Supertest?
Use libraries like Nock to intercept and mock outbound HTTP requests in your tests. This keeps tests fast and isolated from external dependencies while Supertest verifies your API logic.
Can I test APIs requiring authentication like JWT or cookies?
Yes. You can use .auth() for basic auth or .set(‘Authorization’, ‘Bearer <token>’) for JWTs. For session-based flows, use request.agent(app) to persist cookies across requests.
Can Supertest handle multi-step request flows?
Yes. You can write sequential requests using async/await or Promises. This allows you to simulate workflows like login → fetch → update with full control over test flow and data.
How is Supertest different from Postman or other API tools?
Supertest is code-driven, making it ideal for automated testing in CI/CD pipelines. Unlike GUI tools like Postman, Supertest can live within your codebase and is better suited for versioned, repeatable tests.
Does Supertest support file uploads and form data?
Yes. You can use .field() to send form fields and .attach() for uploading files. This is useful when testing endpoints that require multipart/form-data, like image or document uploads.
Author