11 KiB
Backend Rules
Use this document during the E. Parallel Specialized Generation stage
defined in prompts/general-prompt.md.
Purpose
Generate NestJS CRUD artifacts that match the DSL contract exactly and remain compatible with a standard NestJS workspace.
Ownership rule:
- this stage belongs to
generator_nest_resourcesafter contract freeze - parent retains ownership of shared auth strategy, JWT/JWKS design, global backend infra, runtime, and deploy seams
- backend resource generation may attach already-defined auth platform seams where required by the frozen contract, but must not redesign them
Mandatory Inputs
prompts/general-prompt.md- the active
api API.<Entity>block fromdomain/toir.api.dsl - referenced DTOs and enums from
domain/toir.api.dsl - an intact or repaired official NestJS scaffold under
server/
api-summary.json may be consulted only as an auxiliary inventory or validator-related artifact. It must never replace the DSL as the backend source of truth.
Expected Outputs
Per entity:
server/src/modules/<kebab>/<kebab>.module.tsserver/src/modules/<kebab>/<kebab>.controller.tsserver/src/modules/<kebab>/<kebab>.service.tsserver/src/modules/<kebab>/dto/create-<kebab>.dto.tsserver/src/modules/<kebab>/dto/update-<kebab>.dto.ts
Repository-wide:
server/src/app.module.tsregistrations
Scaffold Baseline
- Start backend initialization and repair from the official NestJS CLI workspace, not from hand-written files.
- Preserve Nest workspace essentials:
server/tsconfig.jsonserver/tsconfig.build.jsonserver/nest-cli.jsonserver/src/main.tsserver/src/app.module.ts
- If the workspace is degraded, repair it before generating domain code.
Forbidden patterns:
- hand-written pseudo-Nest scaffolds
- deleting required Nest config files after generation
- replacing normal Nest build/start behavior with ad hoc scripts
Approved Backend Dependency Baseline
When the backend workspace is created or repaired, pin the backend package manifest to these exact versions before continuing:
@nestjs/common:11.1.18@nestjs/core:11.1.18@nestjs/platform-express:11.1.18@nestjs/testing:11.1.18@nestjs/config:4.0.3@nestjs/cli:11.0.17@nestjs/schematics:11.0.10class-validator:0.15.1class-transformer:0.5.1jose:6.2.2reflect-metadata:0.2.2rxjs:7.8.1- backend
typescript:5.7.3
Pinning rules:
- Use exact versions, not
latestand not caret ranges. - Keep the Nest runtime packages on the same approved major/minor line.
- Prisma-specific versions are governed by
prompts/prisma-rules.md.
Route And Resource Contract
- Use the shared entity-to-resource naming convention from
prompts/general-prompt.md. - Each entity becomes a NestJS module, controller, service, and create/update DTO pair.
- CRUD routes use the real primary key name in the path.
- Every API record returned to React Admin must include
id. - For natural-key entities, map the real primary key to
idin responses and sort translation.
DTO Contract
DTO.<Entity>CreatedefinesCreate<Entity>Dto.DTO.<Entity>UpdatedefinesUpdate<Entity>Dto.- Do not invent fields or pull field lists from memory.
- Never include
idin Create/Update DTOs.
Type and decorator rules:
| DSL type | TS DTO type | class-validator decorator | Notes |
|---|---|---|---|
uuid |
string |
@IsUUID() |
|
string |
string |
@IsString() |
|
text |
string |
@IsString() |
|
integer |
number |
@IsInt() |
|
number |
number |
@IsNumber() |
|
decimal |
string |
@IsString() |
serialize with Prisma Decimal |
date |
string |
@IsString() |
serialize as ISO string |
boolean |
boolean |
@IsBoolean() |
|
| enum name | EnumName (imported from ../../enums/<kebab>.enum) |
@IsEnum(EnumName) |
Do not use string |
Nullability rules:
- every field that is not
is requiredgets@IsOptional()before the type decorator - every generated DTO imports from
'class-validator'
Shared Types
Shared TypeScript types are generated by the shared-types stage BEFORE
nest-entities runs. Per-entity modules MUST import them; they must NEVER
redefine them.
Zones:
server/src/enums/<kebab-enum-name>.enum.ts— one file per DSL enumserver/src/shared/pagination.ts—PaginatedResponse<T>andListQueryParamsserver/src/shared/index.ts— barrel for shared types
Import conventions depend on the importing file's depth:
-
From a module/controller/service file at
server/src/modules/<kebab>/<file>.ts:import { EquipmentStatus } from '../../enums/equipment-status.enum'; import { PaginatedResponse, ListQueryParams } from '../../shared/pagination'; -
From a DTO file at
server/src/modules/<kebab>/dto/<file>.ts(one level deeper):import { EquipmentStatus } from '../../../enums/equipment-status.enum'; import { PaginatedResponse, ListQueryParams } from '../../../shared/pagination';
Enum-typed DTO fields MUST be declared with the actual enum type, NOT
string. The enum is imported from the shared file above. @IsEnum(EnumName)
still applies at runtime for validation. Because the DTO type matches the
Prisma-generated enum type by name and by string values, service code can
pass DTOs into Prisma without as EnumName casts.
Auth Import Paths
All auth utilities live under src/auth/ with the following exact import paths (relative to a module file at src/modules/<kebab>/):
| Symbol | Import path |
|---|---|
JwtAuthGuard |
../../auth/guards/jwt-auth.guard |
RolesGuard |
../../auth/guards/roles.guard |
Roles |
../../auth/decorators/roles.decorator |
Public |
../../auth/decorators/public.decorator |
PrismaService |
../../prisma/prisma.service |
Do NOT use ../../auth/jwt-auth.guard, ../../auth/roles.guard, or ../../auth/roles.decorator — those paths do not exist.
Controller Contract
- Apply
@UseGuards(JwtAuthGuard, RolesGuard)at controller class level. - Guard order: JwtAuthGuard must run before RolesGuard to ensure token validation precedes role extraction.
- Roles per verb:
GET->viewer | editor | adminPOST,PATCH,PUT->editor | adminDELETE->admin
- Reconcile DSL HTTP shapes for repository compatibility:
- list endpoints declared as
POST .../pagegenerate as@Get()with React Admin query params - update endpoints declared as
PUTgenerate as@Patch(':<pk>')
- list endpoints declared as
- Path parameters are taken from the DSL endpoint contract, not invented from generic CRUD memory.
Service Contract
- Never pass raw update DTOs directly into Prisma update
data. - Strip
id, the real primary key, and readonly fields before writes. - Keep
PrismaServicelightweight:- extend
PrismaClient - implement
OnModuleInit - call
$connect() - do not add
beforeExit
- extend
Prisma Relation Writes
When a DTO field is a foreign key (e.g. equipmentId: string), never assign it directly on the Prisma data object. Instead, use the connect syntax targeting the relation field name (without the Id suffix):
// WRONG — Prisma rejects scalar FK assignments on relation inputs
data.equipmentId = dto.equipmentId;
// CORRECT — use connect on the relation field
data.equipment = { connect: { id: dto.equipmentId } };
Rule: for any DTO field named <relation>Id where <relation> is a Prisma relation field, map it as { connect: { id: value } } on the relation name in both create and update payloads.
Dynamic Sort Type Assertion
When building orderBy from a runtime sort field, TypeScript requires an explicit cast to avoid TS7053. Always use:
const orderBy: Record<string, 'asc' | 'desc'> = {};
orderBy[sortField] = query._order === 'DESC' ? 'desc' : 'asc';
Do NOT use typed Prisma OrderByWithRelationInput objects for dynamic sort — use Record<string, 'asc' | 'desc'> and pass it directly to Prisma's orderBy.
List endpoint requirements:
- accept React Admin query params:
_start,_end,_sort,_order,q - set
Content-Range: items <start>-<end>/<total>header (RFC 7233 format for items, not bytes) - set
Access-Control-Expose-Headers: Content-Range - return HTTP 200 for successful pagination
Filtering rules:
- string/text filters may use case-insensitive
contains - foreign-key scalar filters must use exact-match semantics
- enum filters must support both single and repeated params
- repeated enum params must map to Prisma
{ in: [...] } _sort=idmust map to the real primary key for natural-key entities
Decimal and date handling:
decimalwrites:new Prisma.Decimal(value)decimalreads:.toString()datewrites:new Date(value)datereads:.toISOString()
Composite Key Rules
When an entity's API endpoints use multiple path parameters (e.g. GET /entity/{fieldA}/{fieldB}), the entity has a composite primary key — there is NO auto-generated id field.
In this case:
- The Prisma model uses
@@id([fieldA, fieldB])— neverid String @id @default(uuid()) - Prisma's
WhereUniqueInputaccepts{ fieldA_fieldB: { fieldA, fieldB } }(underscore-joined) - The NestJS service uses the composite key object, NOT
{ id }:
// WRONG — id does not exist on composite-key entities
where: { id }
// CORRECT — use the Prisma compound unique input
where: { equipmentId_newStatus: { equipmentId, newStatus } }
- Controller route params extract all key fields, not a single
id - Responses include all key fields; React Admin requires a virtual
idfield — map it as a concatenation:id: \${record.equipmentId}_${record.newStatus}``
Natural-Key Rules
For entities whose physical primary key is not id:
- route params use the real primary key name
- responses expose
idmapped from that primary key - sort/update behavior never targets a fake physical
id - update payload sanitization removes both
idand the real primary key
Completion Expectations
Backend generation is incomplete if any of the following is true:
- required Nest scaffold files are missing
- DTO decorators are incomplete or type-incorrect
- controllers are missing guards or role decorators
- natural-key handling regresses to a fake physical
id - list/filter behavior is incompatible with React Admin expectations