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.



