Naalya Handbook

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:

apps/server/src/app/user/controllers/staff.controller.ts
@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:

  • operationIdrequired. 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 / @ApiOkResponse with type: 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:

DecoratorStatus
@ApiNotFoundErrorResponse404 — resource doesn't exist
@ApiConflictErrorResponse409 — e.g. duplicate email
@ApiBadRequestErrorResponse400 — invalid input
@ApiUnauthorizedErrorResponse401 — missing/invalid token
@ApiForbiddenErrorResponse403 — token fine, permission isn't
@ApiAuthErrorResponses401 + 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-validator rule (@IsString, @IsEmail, @IsOptional, @IsUUID, …) — validates incoming data. The global ValidationPipe reads these and rejects bad requests before your handler runs.
  • @ApiProperty (or @ApiPropertyOptional) — puts the field in the OpenAPI schema. A field with no @ApiProperty is invisible to the generated SDK.
  • Add a description only when the field name isn't self-explanatory; otherwise give an example.
apps/server/src/app/user/dto/staff-profile.dto.ts
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:

PathWhat you get
/docsScalar reference (the nice one to read)
/docs/swaggerSwagger UI
/docs/swagger/jsonthe 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

On this page