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