API Conventions
How controllers and DTOs are written so the OpenAPI spec and the generated SDKs stay clean.
Every route you add becomes part of an OpenAPI spec — a machine-readable description of every endpoint — which is used to generate the client SDKs the web and mobile apps call. You never write that spec by hand: it's built at startup from decorators, the @-prefixed annotations NestJS reads as metadata on classes and methods.
So a sloppy route here becomes an ugly method over there. Nothing fails the build if you get it wrong — the spec just quietly degrades (a field drops from the schema, a generated method gets an auto-name). These are enforced in code review, so treat them as required.
Controller class
A controller is the class that handles a group of HTTP routes. Decorators on the class apply to every route inside it:
@Controller('staff')— the URL prefix (/staff)@ApiTags('Staff')— groups these routes in the docs@ApiBearerAuth()— these routes require a JWT@ApiAuthErrorResponses()— adds the shared 401/403 error shapes so you don't repeat them on every method
Handler decorators
Each route method (a handler) carries its own stack of decorators — permission gate → audit → HTTP verb → @ApiOperation → responses:
@RequirePermissions({ action: Action.CREATE, resource: Resource.STAFF })
@Audit({ action: ActionAuditAction.CREATE, resource: AuditResourceType.STAFF })
@Post()
@ApiOperation({ summary: 'Create Staff', operationId: 'createStaff', description: '…' })
@ApiResponse({ status: 201, type: CreateStaffResponseDto })
@ApiConflictErrorResponse('Email already in use.')
async create(@Body() dto: CreateStaffDto) {
return this.staffService.create(dto);
}@ApiOperation describes the endpoint in the docs. Required on every handler, with three keys:
operationId— required. A globally-unique camelCase name (createStaff) that becomes the method name in the generated SDK — so renaming it later breaks every frontend caller. Omit it and the generator invents an ugly name.summary— a short label, ≤3 words.description— the detail: what it does, key params, side effects.
Other handler decorators:
@ApiResponse/@ApiOkResponsewithtype:a response DTO — declares the success shape so it lands in the schema.@CurrentUser('sub')— injects the caller's decoded JWT (here,sub= their user id), so you never parse the token by hand.- Access gates:
@RequirePermissions,@RequireUserType,@RequireSystemRole,@Public— covered in Auth & Permissions. @Audit— records who changed what. Put it on mutating handlers only (create/update/delete), not reads; an interceptor writes the log out-of-band.
Error responses — each of these is a decorator that injects a documented error into the spec. Use the one that fits the failure instead of hand-writing an error body:
| Decorator | Status |
|---|---|
@ApiNotFoundErrorResponse | 404 — resource doesn't exist |
@ApiConflictErrorResponse | 409 — e.g. duplicate email |
@ApiBadRequestErrorResponse | 400 — invalid input |
@ApiUnauthorizedErrorResponse | 401 — missing/invalid token |
@ApiForbiddenErrorResponse | 403 — token fine, permission isn't |
@ApiAuthErrorResponses | 401 + 403 (applied at class level) |
DTOs
A DTO (Data Transfer Object) is the typed shape of a request or response body — a plain class whose fields carry decorators. They live under dto/ in each module. Every field gets two decorators, because each field has two jobs:
- A
class-validatorrule (@IsString,@IsEmail,@IsOptional,@IsUUID, …) — validates incoming data. The globalValidationPipereads these and rejects bad requests before your handler runs. @ApiProperty(or@ApiPropertyOptional) — puts the field in the OpenAPI schema. A field with no@ApiPropertyis invisible to the generated SDK.- Add a
descriptiononly when the field name isn't self-explanatory; otherwise give anexample.
export class StaffProfileDto extends DatabaseEntityDto {
@IsString() @IsNotEmpty()
@ApiProperty({ example: 'John' })
firstName: string;
}Response DTOs extend DatabaseEntityDto to inherit a ready-made, documented id plus timestamps. Create/Update DTOs leave those out — the client never sends id or createdAt; the server assigns them.
A module defines its shape once and reuses it: a *.types.ts interface (a plain TypeScript description of the fields) is the contract; the database entity and the DTOs both implement it, so they can't drift apart. The full rules are in Entities, Tables & Types and DTOs & Validation.
list, not findAll
Name new collection endpoints and service methods list (staffService.list(query)). findAll survives only on the base repository for backward compatibility — don't use it in new controller-facing code.
Serving the spec
The decorators above are assembled into a live, browsable API reference in apps/server/src/main.ts:
| Path | What you get |
|---|---|
/docs | Scalar reference (the nice one to read) |
/docs/swagger | Swagger UI |
/docs/swagger/json | the raw OpenAPI JSON — what the SDK generator consumes |
Outside development, all /docs* routes are behind HTTP Basic Auth (DOCS_USERNAME / DOCS_PASSWORD).
Where to go next
Module Anatomy
The five-layer shape every resource module follows — Entity, Repository, Service, Controller, DTO — and the user-discriminator model.
Error Handling & Logging
How services throw HttpExceptions, how the global filter shapes them into one error envelope, and what the request logger records.