Skip to content

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.

  1. prisma/schema.prisma — add column to the model
  2. Run migrationnpx prisma migrate dev --name add-<field>
  3. src/<domain>/dto/scopes/<scope>.dto.ts — add field to Create*Dto (with validators) and *ResponseDto (with @ApiProperty)
  4. src/common/constants/scope-fields.ts — add field name to the scope's array (e.g. STUDENT_SCOPES.anagraphic)
  5. src/<domain>/<domain>.service.spec.ts — add field to mock factory objects
  6. Verifynpm 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() — like enrollment in 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.

  1. prisma/schema.prisma — add new columns for the scope's fields
  2. Run migrationnpx prisma migrate dev --name add-<scope>-fields
  3. src/<domain>/dto/scopes/<scope>.dto.ts — NEW file with Create*Dto, Update*Dto (via PartialType), and *ResponseDto classes. See src/students/dto/scopes/student-sensitive.dto.ts for pattern.
  4. src/<domain>/dto/create-<domain>.dto.ts — add scope property with @ValidateNested() @Type(() => Create*Dto). Use @IsDefined() if required, @IsOptional() if optional.
  5. src/<domain>/dto/update-<domain>.dto.ts — add scope property with @IsOptional() @ValidateNested() @Type(() => Update*Dto)
  6. src/<domain>/dto/<domain>-response.dto.ts — add @ApiPropertyOptional({ type: *ResponseDto }) property
  7. src/common/constants/scope-fields.ts — add new scope array to the entity's constant (e.g. STUDENT_SCOPES.medical = [...])
  8. src/<domain>/<domain>.service.ts — add scope entry in getScopeFieldMappings() return value (spread the constant array, or use a function for custom shapes)
  9. prisma/seed/rbac-catalogue.ts — add PermissionScope + ScopeFieldMapping entries (using the shared constant) + prisma/seed/roles.ts — add role_permissions grants (WRITE for admin, READ for teacher if appropriate)
  10. src/<domain>/<domain>.service.spec.ts — update mock factories and add tests for new scope fields
  11. Verifynpm 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

  1. src/common/constants/entity-keys.ts — add EntityKey.<ENTITY> + add to CUSTOM_FIELD_ENTITY_KEYS if it has custom fields
  2. prisma/schema.prisma — add model with tenantId FK + customFields Json? (if custom fields needed)
  3. Run migrationnpx prisma migrate dev --name add-<entity>
  4. src/common/constants/scope-fields.ts — add <ENTITY>_SCOPES constant with field arrays per scope
  5. src/<domain>/dto/scopes/ — create scope sub-DTOs: Create*Dto (validators), Update*Dto (PartialType), *ResponseDto (@ApiProperty). Add customFields?: Record<string, unknown> to scope sub-DTOs if custom fields enabled.
  6. src/<domain>/dto/create-<domain>.dto.ts — wrapper DTO composing scope sub-DTOs with @IsDefined() @ValidateNested() @Type()
  7. src/<domain>/dto/update-<domain>.dto.ts — wrapper with @IsOptional() @ValidateNested() @Type() per scope
  8. src/<domain>/dto/<domain>-response.dto.ts — scope-grouped response shape with id, scope groups, createdAt, updatedAt
  9. src/<domain>/<domain>.service.ts — extend BaseTenantedCrudService, implement getPrismaDelegate() and getScopeFieldMappings(). Inject CustomFieldsService if custom fields. Use ScopeMapping: array of field names for simple scopes, function for custom response shapes.
  10. 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.
  11. src/<domain>/<domain>.swagger.ts — swagger decorator compositions using factories from api-crud-helpers.ts
  12. src/<domain>/<domain>.module.ts — NestJS module importing PrismaModule, CustomFieldsModule (if needed), providing + exporting the service
  13. src/<domain>/index.ts — barrel export: module, service, DTOs
  14. src/app.module.ts — import the new module
  15. src/<domain>/<domain>.service.spec.ts — unit tests with mocked PrismaService
  16. prisma/seed/ — seed permission catalogue across modules:
    • rbac-catalogue.tsPermissionEntity, PermissionScope(s) (incl. others scope if custom fields), PermissionAction(s) (create/delete only), ActionScopeRequirement(s), ScopeFieldMapping rows (using the shared constant)
    • roles.tsrole_permissions grants (WRITE for admin, READ for teacher) + RoleActionPermission(s)
    • e2e-fixtures.ts — sample custom field definitions if custom fields enabled
  17. Verifynpm run build && npm test && npx prisma db seed
  18. Update docsCLAUDE.md project structure section, docs/architecture.md if architectural significance

Custom fields integration checklist

If the entity supports custom fields (customFields Json? column):

  1. Add customFields?: Record<string, unknown> to scope sub-DTOs that carry custom fields
  2. Inject CustomFieldsService in entity service constructor
  3. Call validateCustomFields() on create/update
  4. toScopedResponse() handles pickCustomFields() automatically via base service
  5. Seed an others scope 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_EXAMPLES from src/common/constants/error-examples.ts for Swagger examples
  • Base ErrorResponseDto has 5 fields only (statusCode, code, message, timestamp, path) — no params, no data
  • Subclasses add strongly typed params or data — see src/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:

  1. prisma/seed/e2e-fixtures.ts — add a custom_field_definitions row:
  2. tenantId — target tenant
  3. entityKey — must be in CUSTOM_FIELD_ENTITY_KEYS (e.g. 'students')
  4. scopeKey — target scope (defaults to 'others' if omitted at API level)
  5. fieldKey — snake_case identifier (unique per tenant+entity)
  6. label, fieldType (TEXT, NUMBER, DATE, BOOLEAN, SELECT), isRequired, sortOrder
  7. options — JSON array of strings (required for SELECT type only)
  8. Verifynpx 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.

  1. prisma/seed/ — add entries across modules:
  2. rbac-catalogue.tsPermissionAction (entityId, key, label, description) + ActionScopeRequirement(s) (one per required scope, AND logic, WRITE access)
  3. roles.tsRoleActionPermission(s) — grant the action to roles (typically admin role per tenant)
  4. src/<domain>/<domain>.controller.ts — add @RequireAction(EntityKey.<ENTITY>, '<action>') to the route. Do NOT combine with @RequireScopes on the same route.
  5. src/<domain>/<domain>.controller.spec.ts — verify controller test mocks align
  6. Verifynpm 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

  1. src/common/constants/error-codes.ts — add entry to ErrorCode enum (UPPER_SNAKE_CASE)
  2. src/common/constants/error-examples.ts — add entry to ERROR_EXAMPLES object with summary, value: { statusCode, code, message, params?, data?, timestamp, path }
  3. Determine DTO family — which of the 7 typed DTOs carries this code? (see docs/error-codes.md § Typed Swagger Schemas)
  4. Simple code (no params, no data) → add to base ErrorResponseDto enum list in src/common/dto/error-response.dto.ts
  5. Code with params → add to existing subclass enum, or create new subclass + params class in src/common/dto/typed-error-responses.dto.ts + src/common/dto/error-params.dto.ts
  6. Code with data → same pattern with data class
  7. Update barrelssrc/common/dto/index.ts and src/common/index.ts if new classes were added
  8. docs/error-codes.md — add row to the appropriate section table (Authentication, Database, Permission, Setup, etc.)
  9. Verifynpm 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 via Prisma.XGetPayload<{ include: typeof constant }> (compile-time shape validation, no Prisma.validator)
  • Query functions: pure functions that accept PrismaService as 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

  1. Create src/<domain>/<domain>.queries.ts — extract include/select constants (as const), payload types (GetPayload), interfaces, and pure query functions
  2. Update src/<domain>/<domain>.service.ts — import from queries file, remove local type definitions and extracted methods, replace inline includes with constants, replace this.method() calls with imported functions
  3. Verifynpx 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