How to test Node.js APIs with Supertest

Testing API endpoints ensures your REST API behaves correctly, returns proper status codes, and handles errors appropriately. As the creator of CoreUI with over 12 years of Node.js experience since 2014, I’ve built comprehensive API test suites for production services. Supertest is a library specifically designed for testing HTTP servers, allowing you to make requests and assert responses. This approach tests your Express routes without starting an actual server.

Use Supertest with Jest to test Express API endpoints with HTTP requests and response assertions.

Install Supertest:

npm install --save-dev supertest jest

Example API:

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

app.use(express.json())

let users = [
  { id: 1, name: 'John', email: '[email protected]' },
  { id: 2, name: 'Jane', email: '[email protected]' }
]

app.get('/api/users', (req, res) => {
  res.json({ success: true, data: users })
})

app.get('/api/users/:id', (req, res) => {
  const user = users.find(u => u.id === parseInt(req.params.id))
  if (!user) {
    return res.status(404).json({ success: false, error: 'User not found' })
  }
  res.json({ success: true, data: user })
})

app.post('/api/users', (req, res) => {
  const { name, email } = req.body
  if (!name || !email) {
    return res.status(400).json({ success: false, error: 'Name and email required' })
  }
  const newUser = { id: users.length + 1, name, email }
  users.push(newUser)
  res.status(201).json({ success: true, data: newUser })
})

app.delete('/api/users/:id', (req, res) => {
  const index = users.findIndex(u => u.id === parseInt(req.params.id))
  if (index === -1) {
    return res.status(404).json({ success: false, error: 'User not found' })
  }
  users.splice(index, 1)
  res.json({ success: true, message: 'User deleted' })
})

module.exports = app

Test file:

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

describe('User API', () => {
  describe('GET /api/users', () => {
    it('should return all users', async () => {
      const response = await request(app)
        .get('/api/users')
        .expect('Content-Type', /json/)
        .expect(200)

      expect(response.body.success).toBe(true)
      expect(response.body.data).toBeInstanceOf(Array)
      expect(response.body.data.length).toBeGreaterThan(0)
    })
  })

  describe('GET /api/users/:id', () => {
    it('should return user by id', async () => {
      const response = await request(app)
        .get('/api/users/1')
        .expect(200)

      expect(response.body.success).toBe(true)
      expect(response.body.data).toHaveProperty('id', 1)
      expect(response.body.data).toHaveProperty('name')
    })

    it('should return 404 for non-existent user', async () => {
      const response = await request(app)
        .get('/api/users/999')
        .expect(404)

      expect(response.body.success).toBe(false)
      expect(response.body.error).toBe('User not found')
    })
  })

  describe('POST /api/users', () => {
    it('should create new user', async () => {
      const newUser = {
        name: 'Bob',
        email: '[email protected]'
      }

      const response = await request(app)
        .post('/api/users')
        .send(newUser)
        .expect('Content-Type', /json/)
        .expect(201)

      expect(response.body.success).toBe(true)
      expect(response.body.data).toMatchObject(newUser)
      expect(response.body.data).toHaveProperty('id')
    })

    it('should return 400 for invalid data', async () => {
      const response = await request(app)
        .post('/api/users')
        .send({ name: 'Bob' })
        .expect(400)

      expect(response.body.success).toBe(false)
      expect(response.body.error).toContain('required')
    })
  })

  describe('DELETE /api/users/:id', () => {
    it('should delete user', async () => {
      const response = await request(app)
        .delete('/api/users/1')
        .expect(200)

      expect(response.body.success).toBe(true)
      expect(response.body.message).toContain('deleted')
    })
  })

  describe('Authentication', () => {
    it('should require auth token', async () => {
      await request(app)
        .get('/api/protected')
        .expect(401)
    })

    it('should allow with valid token', async () => {
      await request(app)
        .get('/api/protected')
        .set('Authorization', 'Bearer valid-token')
        .expect(200)
    })
  })
})

Advanced testing:

// Test with query parameters
it('should filter users', async () => {
  const response = await request(app)
    .get('/api/users')
    .query({ role: 'admin' })
    .expect(200)
})

// Test file upload
it('should upload file', async () => {
  await request(app)
    .post('/api/upload')
    .attach('file', 'test-file.jpg')
    .expect(200)
})

// Test cookies
it('should set cookie', async () => {
  const response = await request(app)
    .post('/api/login')
    .send({ username: 'test', password: 'pass' })
    .expect(200)

  expect(response.headers['set-cookie']).toBeDefined()
})

Best Practice Note

Supertest doesn’t start a server—it directly calls your Express app for faster tests. Always test both success and error cases for each endpoint. Test status codes, response structure, and data validation. Use beforeEach to reset database state between tests. For database testing, use a test database or in-memory database. Test authentication, authorization, and input validation thoroughly. This is how we test CoreUI backend APIs—comprehensive Supertest suites covering all endpoints, status codes, edge cases, and error conditions for production-ready REST APIs.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author