How to Test APIs with Jest and Supertest: A Complete Guide for Node.js Developers

16/07/2025

How to Test APIs with Jest and Supertest: A Complete Guide for Node.js Developers

Learn how to write reliable API tests in Node.js using Jest and Supertest. This guide covers setup, test structure, mocking, and best practices for testing RESTful endpoints.

How to Test APIs with Jest and Supertest

Ensure the reliability and correctness of your Node.js API endpoints.

In modern backend development, APIs are the backbone of communication between different services and clients. Ensuring that your API endpoints work correctly, handle various inputs gracefully, and return expected outputs is crucial for application stability and reliability. This is where **API testing** becomes indispensable. While manual testing can catch some issues, automated testing provides speed, consistency, and confidence in your codebase. For Node.js applications, **Jest** (a popular JavaScript testing framework) combined with **Supertest** (a library for testing HTTP servers) offers a powerful and intuitive solution for writing comprehensive API tests. This post will guide you through setting up and writing API tests for your Express.js application using these two excellent tools.

Why Test Your APIs?

  • Catch Bugs Early: Identify issues before they reach production.
  • Ensure Correctness: Verify that endpoints return the expected data and status codes.
  • Prevent Regressions: Ensure new features or changes don't break existing functionality.
  • Improve Code Quality: Forces you to write more modular and testable code.
  • Documentation: Tests can serve as living documentation for your API's behavior.

Prerequisites

  • Node.js and npm (or yarn) installed.
  • Basic understanding of Express.js.

1. Project Setup and Dependencies

Create a new Node.js project and install `express`, `jest`, and `supertest`.

# Create project directory
mkdir express-api-testing
cd express-api-testing

# Initialize npm
npm init -y

# Install Express (for our API)
npm install express

# Install Jest and Supertest (as dev dependencies)
npm install --save-dev jest supertest

Update your `package.json` to include a test script:

// package.json
{
  "name": "express-api-testing",
  "version": "1.0.0",
  "description": "",
  "main": "server.js",
  "scripts": {
    "start": "node server.js",
    "test": "jest" // Add this line
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.18.2"
  },
  "devDependencies": {
    "jest": "^29.7.0",
    "supertest": "^6.3.3"
  }
}

2. Create a Simple Express API (`server.js`)

We'll create a basic Express application with a few endpoints to test.

// server.js
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;

// Middleware to parse JSON bodies
app.use(express.json());

// In-memory data store for demonstration
let users = [
  { id: 1, name: 'Alice', email: 'alice@example.com' },
  { id: 2, name: 'Bob', email: 'bob@example.com' },
];
let nextUserId = 3;

// GET /api/users - Get all users
app.get('/api/users', (req, res) => {
  res.status(200).json(users);
});

// GET /api/users/:id - Get a single user by ID
app.get('/api/users/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const user = users.find(u => u.id === id);
  if (user) {
    res.status(200).json(user);
  } else {
    res.status(404).json({ message: 'User not found' });
  }
});

// POST /api/users - Create a new user
app.post('/api/users', (req, res) => {
  const { name, email } = req.body;
  if (!name || !email) {
    return res.status(400).json({ message: 'Name and email are required' });
  }
  const newUser = { id: nextUserId++, name, email };
  users.push(newUser);
  res.status(201).json(newUser);
});

// Export the app for testing
module.exports = app;

// Only start the server if this file is run directly (not imported by tests)
if (require.main === module) {
  app.listen(PORT, () => {
    console.log(`Server running on http://localhost:${PORT}`);
  });
}

3. Writing Your API Tests (`__tests__/users.test.js`)

Create a `__tests__` directory in your project root and a file named `users.test.js` inside it.

// __tests__/users.test.js
const request = require('supertest');
const app = require('../server'); // Import your Express app

// Mock data (optional, but good for isolated tests)
let initialUsers = [
    { id: 1, name: 'Alice', email: 'alice@example.com' },
    { id: 2, name: 'Bob', email: 'bob@example.com' },
];

// Before each test, reset the users array to a known state
beforeEach(() => {
    // This is a simple way to reset in-memory data for testing.
    // In a real app, you'd typically clear/seed your test database.
    app._router.stack.forEach(function(r){
        if (r.route && r.route.path === '/api/users' && r.route.methods.get) {
            r.handle = (req, res) => res.status(200).json(initialUsers);
        }
        if (r.route && r.route.path === '/api/users/:id' && r.route.methods.get) {
            r.handle = (req, res) => {
                const id = parseInt(req.params.id);
                const user = initialUsers.find(u => u.id === id);
                if (user) {
                    res.status(200).json(user);
                } else {
                    res.status(404).json({ message: 'User not found' });
                }
            };
        }
        if (r.route && r.route.path === '/api/users' && r.route.methods.post) {
            r.handle = (req, res) => {
                const { name, email } = req.body;
                if (!name || !email) {
                    return res.status(400).json({ message: 'Name and email are required' });
                }
                const newUser = { id: initialUsers.length + 1, name, email }; // Simple ID generation
                initialUsers.push(newUser);
                res.status(201).json(newUser);
            };
        }
    });
});


describe('GET /api/users', () => {
    test('should return all users', async () => {
        const res = await request(app).get('/api/users');
        expect(res.statusCode).toEqual(200);
        expect(res.body).toEqual(initialUsers);
        expect(Array.isArray(res.body)).toBeTruthy();
        expect(res.body.length).toEqual(2);
    });

    test('should return a single user by ID', async () => {
        const res = await request(app).get('/api/users/1');
        expect(res.statusCode).toEqual(200);
        expect(res.body).toEqual({ id: 1, name: 'Alice', email: 'alice@example.com' });
    });

    test('should return 404 if user not found', async () => {
        const res = await request(app).get('/api/users/999');
        expect(res.statusCode).toEqual(404);
        expect(res.body).toEqual({ message: 'User not found' });
    });
});

describe('POST /api/users', () => {
    test('should create a new user', async () => {
        const newUser = { name: 'Charlie', email: 'charlie@example.com' };
        const res = await request(app)
            .post('/api/users')
            .send(newUser); // Send the request body

        expect(res.statusCode).toEqual(201); // 201 Created
        expect(res.body).toHaveProperty('id');
        expect(res.body.name).toEqual(newUser.name);
        expect(res.body.email).toEqual(newUser.email);
        // Verify that the user was actually added to our mock data
        expect(initialUsers.length).toEqual(3);
        expect(initialUsers[2]).toMatchObject(newUser);
    });

    test('should return 400 if name is missing', async () => {
        const res = await request(app)
            .post('/api/users')
            .send({ email: 'test@example.com' });

        expect(res.statusCode).toEqual(400);
        expect(res.body).toEqual({ message: 'Name and email are required' });
    });

    test('should return 400 if email is missing', async () => {
        const res = await request(app)
            .post('/api/users')
            .send({ name: 'Test User' });

        expect(res.statusCode).toEqual(400);
        expect(res.body).toEqual({ message: 'Name and email are required' });
    });
});

// Example of testing a protected route (conceptual, requires auth middleware)
// describe('GET /api/protected', () => {
//     test('should return 401 if no token is provided', async () => {
//         const res = await request(app).get('/api/protected');
//         expect(res.statusCode).toEqual(401);
//     });

//     test('should return 200 if valid token is provided', async () => {
//         // You would typically mock your authentication middleware here
//         // or generate a valid token for testing purposes.
//         // For example, if using JWT:
//         // const token = jwt.sign({ id: 1 }, 'test_secret');
//         const res = await request(app)
//             .get('/api/protected')
//             .set('Authorization', 'Bearer MOCKED_VALID_TOKEN'); // Set authorization header
//         expect(res.statusCode).toEqual(200);
//     });
// });
```

4. Running Your Tests

Navigate to your project root in the terminal and run the test script:

npm test

Jest will discover and run all files ending with `.test.js` (or `.spec.js`) in your project. You should see output indicating that your tests passed.

Best Practices for API Testing

  • Isolate Tests: Ensure each test runs independently and doesn't affect other tests. For database-backed APIs, this often means clearing and seeding the database before each test or test suite.
  • Test Edge Cases: Don't just test happy paths. Test invalid inputs, missing fields, unauthorized access, and edge conditions.
  • Mock External Dependencies: For services that rely on external APIs, databases, or third-party services, mock them to ensure your tests are fast and reliable.
  • Use Descriptive Names: Give your test files and `describe`/`test` blocks clear, descriptive names that explain what they are testing.
  • Test Status Codes: Always assert the HTTP status code (e.g., 200, 201, 400, 404).
  • Test Response Body: Assert that the response body contains the expected data and structure.
  • Organize Tests: Group related tests using `describe` blocks.

Automated API testing with Jest and Supertest is a powerful combination for building robust and reliable Node.js applications. By writing comprehensive tests for your endpoints, you gain confidence in your API's behavior, prevent regressions, and streamline your development workflow. This guide provides a solid foundation for getting started with API testing. Remember to continuously expand your test suite as your API evolves, covering all critical paths and edge cases to ensure long-term stability and maintainability.

Building robust APIs is only half the battle — the other half is ensuring they work consistently over time. Whether you're releasing new features or refactoring old code, automated API testing helps catch regressions, enforce contracts, and build developer confidence.

Two of the most powerful tools for testing APIs in Node.js are Jest and Supertest. Jest is a fast, zero-config testing framework maintained by Meta, while Supertest is a high-level abstraction for HTTP assertions that works seamlessly with Express and other Node.js HTTP servers.