Part 4.3 — Integration Testing with Supertest: REST Endpoints
Unit tests validate logic in isolation.
Integration tests validate reality.
Supertest lets you make real HTTP requests against your NestJS backend — without running the server on a real port — giving you confidence that:
controllers are wired correctly
pipes validate inputs
class-validator catches invalid DTOs
filters return proper error shapes
interceptors log/transform responses
Prisma queries run on a real test PostgreSQL DB
By the end of this post, you'll have fully tested REST endpoints that behave exactly as your frontend expects.
1. Bootstrapping NestJS for Integration Tests
Each integration suite begins by:
Creating a testing module
Compiling it
Initializing a Nest application instance
Getting the HTTP server
Example:
let app;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleRef.createNestApplication();
await app.init();
});This gives Supertest a live API surface to hit.
2. Import Supertest
import request from 'supertest';Use:
await request(app.getHttpServer())
.get('/users')
.expect(200);No need to listen on a port — the server is created in-memory.
3. Ensure the Test Database Is Clean Before Each Suite
You built resetDb() in Part 4.1.
Use:
beforeEach(async () => {
await resetDb();
});Never let tests leak data across suites.
4. Testing a POST Endpoint (Users)
This test ensures:
DTO validation runs
Pipes transform input
Prisma writes to DB
Global exception filter shapes output
describe('POST /users', () => {
it('creates a user', async () => {
const res = await request(app.getHttpServer())
.post('/users')
.send({ name: 'Alice', email: 'alice@example.com' })
.expect(201);
expect(res.body.email).toBe('alice@example.com');
expect(res.body.id).toBeDefined();
});
it('rejects invalid DTOs', async () => {
const res = await request(app.getHttpServer())
.post('/users')
.send({ name: '', email: 'invalid' })
.expect(400);
expect(res.body.message).toContain('email must be an email');
});
});This tests the entire request pipeline.
5. Testing a GET List Endpoint
describe('GET /users', () => {
it('returns all users', async () => {
await prisma.user.create({
data: { name: 'Bob', email: 'bob@example.com' },
});
const res = await request(app.getHttpServer())
.get('/users')
.expect(200);
expect(res.body.length).toBe(1);
expect(res.body[0].name).toBe('Bob');
});
});This validates your service → prisma → DB flow.
6. Testing GET by ID With ParseIntPipe
Because you used ParseIntPipe:
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) { ... }Test both success & failure:
it('rejects non-numeric IDs', async () => {
const res = await request(app.getHttpServer())
.get('/users/abc')
.expect(400);
expect(res.body.message).toContain('Validation failed');
});Properly typed IDs matter for Prisma queries.
7. Testing PATCH Endpoint
describe('PATCH /users/:id', () => {
it('updates a user', async () => {
const user = await prisma.user.create({
data: { name: 'Alice', email: 'alice@example.com' },
});
const res = await request(app.getHttpServer())
.patch(`/users/${user.id}`)
.send({ name: 'Updated' })
.expect(200);
expect(res.body.name).toBe('Updated');
});
});Test invalid updates too.
8. Testing DELETE Endpoint
describe('DELETE /users/:id', () => {
it('deletes a user', async () => {
const user = await prisma.user.create({
data: { name: 'Bob', email: 'bob@example.com' },
});
await request(app.getHttpServer())
.delete(`/users/${user.id}`)
.expect(200);
const exists = await prisma.user.findUnique({
where: { id: user.id },
});
expect(exists).toBeNull();
});
});Validating deletion is surprisingly important — especially for relational tables.
9. Testing Nested Relations (Posts → Author)
describe('GET /posts', () => {
it('includes authors', async () => {
const user = await prisma.user.create({
data: { name: 'Alice', email: 'alice@example.com' },
});
await prisma.post.create({
data: { title: 'Hello', body: 'Test', authorId: user.id },
});
const res = await request(app.getHttpServer())
.get('/posts')
.expect(200);
expect(res.body[0].author.email).toBe('alice@example.com');
});
});This ensures Prisma relations work in full-stack tests.
10. Testing Expected Error Shapes
Because you implemented a global exception filter, test its shape:
it('returns unified error format', async () => {
const res = await request(app.getHttpServer())
.get('/users/999') // nonexistent
.expect(404);
expect(res.body).toHaveProperty('statusCode');
expect(res.body).toHaveProperty('message');
expect(res.body).toHaveProperty('timestamp');
});Consistency is everything.
11. Test Real Validation Flow (DTO + Pipes)
Example:
it('validates DTOs through global pipes', async () => {
const res = await request(app.getHttpServer())
.post('/posts')
.send({ title: '', body: '', authorId: 'abc' })
.expect(400);
expect(res.body.message.some(m => m.includes('must not be empty'))).toBe(true);
});This verifies your DTO → ValidationPipe → ExceptionFilter chain.
12. Best Practices for Integration Tests
isolate database between tests
keep tests small but realistic
avoid mocking (except external services)
test happy paths and failure paths
assert response shape, not just status codes
prefer
.expect({ ... })when shape is stable
Integration tests are the guarantee that your entire backend behaves the way your frontend expects.
13. Summary
You now know how to:
bootstrap NestJS test applications
hit REST endpoints with Supertest
validate entire request pipelines
test DTO validation + pipes
confirm error filters behave consistently
test relational and pagination behaviors
isolate test databases cleanly
Related
Part 4.2 — Writing Unit Tests for Services, DTOs & Pipes
In this post, you’ll learn how to isolate and test NestJS services, validate DTOs programmatically, and verify custom pipes with clear testing strategies.
Part 4.1 — Setting Up Jest, Test Environment & Prisma Test DB
Before you write your first test, you need a stable foundation: Jest configuration, test modules, a sandboxed PostgreSQL database, and a clean Prisma workflow for reproducible tests.
Part 3.4 — Error Handling, Pipes, Filters & Global Interceptors
Production backends thrive on predictable errors, structured validation, and consistent responses. In this post, you’ll learn how to build those foundations using NestJS pipes, filters, and interceptors.
Comments