Skip to content

Part 4.3 — Integration Testing with Supertest: REST Endpoints

Site Console Site Console
4 min read Updated Dec 10, 2025 Backend Development 0 comments

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:

  1. Creating a testing module

  2. Compiling it

  3. Initializing a Nest application instance

  4. 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

Leave a comment

Sign in to leave a comment.

Comments