Common Workflows¶
Quick-reference checklists for frequent operations. Each lists the exact files to touch.
For architecture context, see docs/architecture.md. Canonical CRUD examples: src/students/.
1. Add a Field to an Existing Scope¶
Example: adding middleName to students.anagraphic.
prisma/schema.prisma— add column to the model- Run migration —
npx prisma migrate dev --name add-<field> src/<domain>/dto/scopes/<scope>.dto.ts— add field toCreate*Dto(with validators) and*ResponseDto(with@ApiProperty)src/common/constants/scope-fields.ts— add field name to the scope's array (e.g.STUDENT_SCOPES.anagraphic)src/<domain>/<domain>.service.spec.ts— add field to mock factory objects- Verify —
npm run build && npm test
No seed changes needed — ScopeFieldMapping rows in seed use the shared constant arrays, so they pick up the new field automatically.
Note: If the scope uses a function mapping (not an array) in
getScopeFieldMappings()— likeenrollmentin StudentsService — also update the function to include the new field.
2. Add a New Scope to an Existing Entity¶
Example: adding a medical scope to students.
prisma/schema.prisma— add new columns for the scope's fields- Run migration —
npx prisma migrate dev --name add-<scope>-fields src/<domain>/dto/scopes/<scope>.dto.ts— NEW file withCreate*Dto,Update*Dto(viaPartialType), and*ResponseDtoclasses. Seesrc/students/dto/scopes/student-sensitive.dto.tsfor pattern.src/<domain>/dto/create-<domain>.dto.ts— add scope property with@ValidateNested() @Type(() => Create*Dto). Use@IsDefined()if required,@IsOptional()if optional.src/<domain>/dto/update-<domain>.dto.ts— add scope property with@IsOptional() @ValidateNested() @Type(() => Update*Dto)src/<domain>/dto/<domain>-response.dto.ts— add@ApiPropertyOptional({ type: *ResponseDto })propertysrc/common/constants/scope-fields.ts— add new scope array to the entity's constant (e.g.STUDENT_SCOPES.medical = [...])src/<domain>/<domain>.service.ts— add scope entry ingetScopeFieldMappings()return value (spread the constant array, or use a function for custom shapes)prisma/seed/rbac-catalogue.ts— addPermissionScope+ScopeFieldMappingentries (using the shared constant) +prisma/seed/roles.ts— addrole_permissionsgrants (WRITE for admin, READ for teacher if appropriate)src/<domain>/<domain>.service.spec.ts— update mock factories and add tests for new scope fields- Verify —
npm run build && npm test && npx prisma db seed
3. Add a New Domain Entity (CRUD Module)¶
File structure¶
src/<domain>/
├── <domain>.module.ts
├── <domain>.controller.ts
├── <domain>.swagger.ts # Swagger decorator compositions (sibling-only, not exported)
├── <domain>.service.ts
├── <domain>.service.spec.ts
├── dto/
│ ├── scopes/ # Scope sub-DTOs (one per scope)
│ ├── create-<domain>.dto.ts # Composes scope sub-DTOs with @ValidateNested
│ ├── update-<domain>.dto.ts # PartialType of scope sub-DTOs
│ └── <domain>-response.dto.ts # Scope-grouped response shape
├── interfaces/ # Internal contracts (never exported)
└── index.ts # Barrel export: module, service, DTOs
Canonical examples¶
Read these files for the reference pattern:
- Controller: src/students/students.controller.ts
- Service: src/students/students.service.ts
- Swagger: src/students/students.swagger.ts
- Scope DTOs: src/students/dto/scopes/
- Response DTO: src/students/dto/student-response.dto.ts
- CRUD swagger factories: src/common/decorators/api-crud-helpers.ts
Steps¶
src/common/constants/entity-keys.ts— addEntityKey.<ENTITY>+ add toCUSTOM_FIELD_ENTITY_KEYSif it has custom fieldsprisma/schema.prisma— add model withtenantIdFK +customFields Json?(if custom fields needed)- Run migration —
npx prisma migrate dev --name add-<entity> src/common/constants/scope-fields.ts— add<ENTITY>_SCOPESconstant with field arrays per scopesrc/<domain>/dto/scopes/— create scope sub-DTOs:Create*Dto(validators),Update*Dto(PartialType),*ResponseDto(@ApiProperty). AddcustomFields?: Record<string, unknown>to scope sub-DTOs if custom fields enabled.src/<domain>/dto/create-<domain>.dto.ts— wrapper DTO composing scope sub-DTOs with@IsDefined() @ValidateNested() @Type()src/<domain>/dto/update-<domain>.dto.ts— wrapper with@IsOptional() @ValidateNested() @Type()per scopesrc/<domain>/dto/<domain>-response.dto.ts— scope-grouped response shape withid, scope groups,createdAt,updatedAtsrc/<domain>/<domain>.service.ts— extendBaseTenantedCrudService, implementgetPrismaDelegate()andgetScopeFieldMappings(). InjectCustomFieldsServiceif custom fields. UseScopeMapping: array of field names for simple scopes, function for custom response shapes.src/<domain>/<domain>.controller.ts—@ProtectedResource()at class level. Routes: POST with@RequireAction(entity, 'create'), GET list/detail with@RequireScopes(entity, 'read'), PATCH with@RequireScopes(entity, 'write'), DELETE with@HttpCode(204) @RequireAction(entity, 'delete'). JSDoc/** */on each method.src/<domain>/<domain>.swagger.ts— swagger decorator compositions using factories fromapi-crud-helpers.tssrc/<domain>/<domain>.module.ts— NestJS module importingPrismaModule,CustomFieldsModule(if needed), providing + exporting the servicesrc/<domain>/index.ts— barrel export: module, service, DTOssrc/app.module.ts— import the new modulesrc/<domain>/<domain>.service.spec.ts— unit tests with mocked PrismaServiceprisma/seed/— seed permission catalogue across modules:rbac-catalogue.ts—PermissionEntity,PermissionScope(s)(incl.othersscope if custom fields),PermissionAction(s)(create/deleteonly),ActionScopeRequirement(s),ScopeFieldMappingrows (using the shared constant)roles.ts—role_permissionsgrants (WRITE for admin, READ for teacher) +RoleActionPermission(s)e2e-fixtures.ts— sample custom field definitions if custom fields enabled
- Verify —
npm run build && npm test && npx prisma db seed - Update docs —
CLAUDE.mdproject structure section,docs/architecture.mdif architectural significance
Custom fields integration checklist¶
If the entity supports custom fields (customFields Json? column):
- Add
customFields?: Record<string, unknown>to scope sub-DTOs that carry custom fields - Inject
CustomFieldsServicein entity service constructor - Call
validateCustomFields()on create/update toScopedResponse()handlespickCustomFields()automatically via base service- Seed an
othersscope for the entity (default scope for custom fields)
Custom-field-only scopes (like others) need zero entity service changes — auto-discovered from definitions in toScopedResponse().
Error response pattern¶
7 typed Swagger DTOs (see docs/error-codes.md § Typed Swagger Schemas):
| Status | DTO to use | When |
|---|---|---|
| 400 (validation) | ValidationErrorResponseDto |
Class-validator failures |
| 400 (setup) | SetupValidationErrorResponseDto, StepIncompleteErrorResponseDto, or base ErrorResponseDto |
Step-specific validation, setup navigation |
| 401, 403, 429 | ErrorResponseDto (base) |
Auth, permission, rate-limit errors |
| 404, 409 | EntityErrorResponseDto |
Not found, unique constraint |
| 422 | ImportErrorResponseDto |
Import file validation |
- CRUD factories (
api-crud-helpers.ts) auto-wire the correct DTOs — no per-endpoint override needed for standard CRUD ApiForbiddenResponses()takes string keys ('insufficientScope','actionNotPermitted','forbiddenFields')ERROR_EXAMPLESfromsrc/common/constants/error-examples.tsfor Swagger examples- Base
ErrorResponseDtohas 5 fields only (statusCode,code,message,timestamp,path) — noparams, nodata - Subclasses add strongly typed
paramsordata— seesrc/common/dto/typed-error-responses.dto.ts - Register all referenced error DTOs via
ApiExtraModels()in controller swagger decorators
4. Add a Custom Field Definition¶
Custom field definitions are seeded data — runtime CRUD is handled by src/custom-fields/. To seed a new definition:
prisma/seed/e2e-fixtures.ts— add acustom_field_definitionsrow:tenantId— target tenantentityKey— must be inCUSTOM_FIELD_ENTITY_KEYS(e.g.'students')scopeKey— target scope (defaults to'others'if omitted at API level)fieldKey— snake_case identifier (unique per tenant+entity)label,fieldType(TEXT,NUMBER,DATE,BOOLEAN,SELECT),isRequired,sortOrderoptions— JSON array of strings (required forSELECTtype only)- Verify —
npx prisma db seed
No code changes needed — the custom-fields module discovers definitions dynamically. If the target entity doesn't have a customFields Json? column yet, see the custom fields integration checklist in workflow #3.
5. Add a Permission Action¶
Actions are for binary operations only (create, delete). Do NOT add update actions — scope WRITE already handles field-level write access.
prisma/seed/— add entries across modules:rbac-catalogue.ts—PermissionAction(entityId,key,label,description) +ActionScopeRequirement(s)(one per required scope, AND logic, WRITE access)roles.ts—RoleActionPermission(s)— grant the action to roles (typically admin role per tenant)src/<domain>/<domain>.controller.ts— add@RequireAction(EntityKey.<ENTITY>, '<action>')to the route. Do NOT combine with@RequireScopeson the same route.src/<domain>/<domain>.controller.spec.ts— verify controller test mocks align- Verify —
npm run build && npm test && npx prisma db seed
See prisma/seed/rbac-catalogue.ts students create/delete actions for the canonical seed pattern.
6. Add an Error Code¶
src/common/constants/error-codes.ts— add entry toErrorCodeenum (UPPER_SNAKE_CASE)src/common/constants/error-examples.ts— add entry toERROR_EXAMPLESobject withsummary,value: { statusCode, code, message, params?, data?, timestamp, path }- Determine DTO family — which of the 7 typed DTOs carries this code? (see
docs/error-codes.md§ Typed Swagger Schemas) - Simple code (no params, no data) → add to base
ErrorResponseDtoenum list insrc/common/dto/error-response.dto.ts - Code with
params→ add to existing subclass enum, or create new subclass + params class insrc/common/dto/typed-error-responses.dto.ts+src/common/dto/error-params.dto.ts - Code with
data→ same pattern with data class - Update barrels —
src/common/dto/index.tsandsrc/common/index.tsif new classes were added docs/error-codes.md— add row to the appropriate section table (Authentication, Database, Permission, Setup, etc.)- Verify —
npm run build
Use AppException to throw the new code:
throw new AppException(ErrorCode.YOUR_CODE, 'Human-readable message', HttpStatus.XXX);
// With params:
throw new AppException(ErrorCode.YOUR_CODE, 'msg', HttpStatus.XXX, { params: { key: 'value' } });
7. Extract Queries to *.queries.ts¶
A module gets a sibling *.queries.ts file when it has 2+ non-trivial query shapes or reusable where-builders. This separates data-access shapes from business logic without introducing a repository pattern.
Convention¶
- File location:
src/<domain>/<domain>.queries.ts— sibling to the service - Include/select constants: use
as const, derive payload types viaPrisma.XGetPayload<{ include: typeof constant }>(compile-time shape validation, noPrisma.validator) - Query functions: pure functions that accept
PrismaServiceas first param (compatible with transaction clients). No business logic, no exceptions — just data loading. - Barrel exports: queries files are sibling-only imports (NOT exported from module
index.ts) unless cross-module usage exists - Business logic stays in service: exception throwing, permission checks, validation — all remain in the service
Steps¶
- Create
src/<domain>/<domain>.queries.ts— extract include/select constants (as const), payload types (GetPayload), interfaces, and pure query functions - Update
src/<domain>/<domain>.service.ts— import from queries file, remove local type definitions and extracted methods, replace inline includes with constants, replacethis.method()calls with imported functions - Verify —
npx tsc --noEmit && npx jest --testPathPatterns="<module>" --no-coverage
Canonical examples¶
- Constants only:
src/users/users.queries.ts,src/custom-fields/custom-fields.queries.ts,src/auth/auth.queries.ts - Constants + functions:
src/permissions/permissions.queries.ts,src/students/students.queries.ts