Skip to content

Writing API E2E Tests

API end-to-end tests live in apps/api-e2e/ and use Jest to spin up a real NestJS application against a dedicated test database. Each test makes actual HTTP requests — there is no mocking of the database or business logic.


File structure

apps/api-e2e/src/
├── api/
│   ├── auth/auth.spec.ts
│   ├── products/products.spec.ts
│   ├── users/users.spec.ts
│   └── <module>/<module>.spec.ts     ← your new spec goes here
├── helpers/
│   ├── login.helper.ts               ← authenticate a user, get JWT
│   ├── product.helper.ts             ← HTTP wrappers for product endpoints
│   ├── vendor.helper.ts
│   └── storefront.helper.ts
├── support/
│   ├── utilities.ts                  ← createTestApplication, createTestAccount, initializeDatabaseOptimized
│   ├── core-testing.module.ts        ← shared NestJS test module (DB, config, events)
│   ├── global-setup.ts               ← enforces NODE_ENV=test, loads .env.test
│   └── global-teardown.ts
└── utils/
    └── product.utils.ts              ← test data builders

Anatomy of a spec file

Every spec follows the same structure. Use this as your starting point:

import { MikroORM } from '@mikro-orm/mysql';
import { NestFastifyApplication } from '@nestjs/platform-fastify';
import { createTestAccount, createTestApplication, initializeDatabaseOptimized } from '../../support/utilities';
import { CoreTestingModule } from '../../support/core-testing.module';
import { AuthModule } from '../../../../api/src/auth/auth.module';
import { OrdersModule } from '../../../../api/src/orders/orders.module'; // ← your module
import { UserRole } from '../../../../../libs/shared/src';
import { loginUser } from '../../helpers/login.helper';

describe('Orders (e2e)', () => {
  let app: NestFastifyApplication;
  let orm: MikroORM;

  let vendorAuthResponse: AuthResponse;

  beforeEach(async () => {
    // Boot a real NestJS app with only the modules this spec needs
    app = await createTestApplication({
      imports: [CoreTestingModule, AuthModule, OrdersModule],
    });

    orm = app.get(MikroORM);
    await initializeDatabaseOptimized(orm); // wipes and recreates schema for clean state

    const password = 'test1234';
    const { user, vendor, storefront } = await createTestAccount(orm.em, UserRole.VENDOR, password);
    vendorAuthResponse = await loginUser(app, user, password, storefront.url);
  }, 30000); // allow 30s for app boot + DB setup

  afterAll(async () => {
    await app?.close();
    await orm?.close();
  });

  it('should be defined', () => {
    expect(app).toBeDefined();
  });

  describe('GET /orders', () => {
    it('should return paginated orders for authenticated vendor', async () => {
      const res = await app.inject({
        method: 'GET',
        url: '/orders',
        headers: { Authorization: `Bearer ${vendorAuthResponse.accessToken}` },
      });

      expect(res.statusCode).toBe(200);
      const json = res.json();
      expect(json).toHaveProperty('data');
      expect(json).toHaveProperty('meta');
    });

    it('should return 401 when no token is provided', async () => {
      const res = await app.inject({ method: 'GET', url: '/orders' });
      expect(res.statusCode).toBe(401);
    });
  });
});

Key utilities

createTestApplication(metadata)

Boots a NestJS app using only the modules listed in imports. Only import what the spec needs — this keeps boot time fast.

app = await createTestApplication({
  imports: [CoreTestingModule, AuthModule, OrdersModule],
});

initializeDatabaseOptimized(orm)

Drops and recreates the entire schema before each test. This guarantees a clean database state and prevents test pollution. Always call this in beforeEach.

createTestAccount(em, role, password)

Creates a user, and for UserRole.VENDOR also creates a vendor and storefront. Returns { user, vendor, storefront }.

const { user, vendor, storefront } = await createTestAccount(orm.em, UserRole.VENDOR, 'password');

loginUser(app, user, password, storefrontUrl)

POSTs to /auth/login and returns an AuthResponse containing accessToken. Use this to get a token for protected endpoints.


Writing a helper file

For each new module, create a helper file at apps/api-e2e/src/helpers/<module>.helper.ts. Helpers are thin HTTP wrappers — they keep spec files clean and make requests reusable across specs.

// helpers/order.helper.ts
import { NestFastifyApplication } from '@nestjs/platform-fastify';

export const getOrders = async (app: NestFastifyApplication, token?: string) => {
  const res = await app.inject({
    method: 'GET',
    url: '/orders',
    headers: token ? { Authorization: `Bearer ${token}` } : undefined,
  });
  return { status: res.statusCode, json: res.json() };
};

export const createOrder = async (app: NestFastifyApplication, payload: object, token: string) => {
  const res = await app.inject({
    method: 'POST',
    url: '/orders',
    headers: { Authorization: `Bearer ${token}` },
    payload,
  });
  return { status: res.statusCode, json: res.json() };
};

What to test

Every spec should cover these scenarios at minimum:

Scenario Expected status
Happy path — authenticated, valid data 200 / 201
No auth token 401
Wrong user / wrong ownership 403
Resource not found 404
Invalid or missing fields 400

Pagination responses should always verify the meta object:

expect(json.meta).toHaveProperty('total');
expect(json.meta).toHaveProperty('totalPages');
expect(json.meta).toHaveProperty('page');
expect(json.meta).toHaveProperty('limit');

Running your spec

# Run the full API e2e suite
pnpm nx e2e api-e2e

# Run only your new spec
pnpm nx e2e api-e2e --testPathPattern orders.spec

# Run a single test within your spec
pnpm nx e2e api-e2e --testNamePattern "should return 401"

Common mistakes

Schema not recreated between tests — always call initializeDatabaseOptimized(orm) in beforeEach, not beforeAll. Using beforeAll causes test state to leak between cases.

Too many modules imported — only import modules your spec actually tests. Importing the entire app slows boot time significantly.

Missing afterAll cleanup — always close the app and ORM connection in afterAll. Leaving connections open causes Jest to hang after the suite completes.

Testing implementation details — test HTTP behaviour (status codes, response shape), not internal service logic. If you find yourself mocking a service method, the test belongs in a unit test instead.