Nest apps
Rules for NestJS apps, currently apps/api.
Layout
src/features/<names>/: one feature each, named in the plural. Files:<name>.module.ts,.controller.ts,.service.ts, an optional.repository.ts,dto/,response/,libs/,index.ts.src/common/<module>/: code that two or more features use. Import it through its barrel; a module over five files splits into role folders with their own barrels.- Filenames are kebab-case with a Nest role suffix:
.service.ts,.guard.ts,.dto.ts. - Queries shared with other apps live in
@openperf/server; their feature has no repository.
Requests and responses
- Schemas are zod:
dto/<name>.dto.tsexports<name>Dto,response/<name>.response.tsexports<name>Response. Types come fromz.infer. - Declare both sides on the handler:
@Query({ schema })or@Body({ schema }),@ApiResult(schema), and@ApiErrors()on the controller. - Body and response schemas get
.meta({ id }); query schemas don’t. Response schemas have no.transform(); time-based defaults belong in the request. - Errors carry a stable code from
buildApiError;GlobalExceptionFilterbuilds the response.
Access
- Every route declares
@Access("read" | "write" | "authenticated")or@Public(true), on the controller and per handler where it differs.src/app.module.test.tschecks every route. - API tokens reach only
@AllowApiToken(true)routes, and read tokens can’t write. Read the caller with@Actor().
Code
- Only entry points call
readEnvironment(). Providers injectENVIRONMENT, functions take values as parameters, tests usecreateTestEnvironment(). - Slow or retried work goes to worker-jobs through
JobQueueService. Pass the transaction when the job must commit with a row change. - Never
import typea class used for dependency injection. - Route handlers and migrations are exempt from verb-first names. Prefer Bun APIs.