Error Code Convention¶
The backend returns structured error responses with a stable code field. The frontend uses this code as the i18n key to display localized error messages. The optional params field provides interpolation values.
Response Shape¶
Every error response has at least these 5 base fields:
{
"statusCode": 404,
"code": "NOT_FOUND",
"message": "Student not found",
"timestamp": "2026-03-10T12:00:00.000Z",
"path": "/api/v1/students/abc-123"
}
Some error codes add typed params or data fields — see Typed Swagger Schemas below.
| Field | Description |
|---|---|
statusCode |
HTTP status code |
code |
Stable, machine-readable string. Frontend uses this as i18n key |
message |
English human-readable string for logs/debugging |
params |
Optional typed object — interpolation values for the frontend translation template (only on specific DTOs) |
data |
Optional typed payload — structured details like validation errors (only on specific DTOs) |
timestamp |
ISO 8601 timestamp |
path |
Request URL path |
Frontend translation example:
{
"NOT_FOUND": "{{entity}} not found",
"SETUP_STEP_MISMATCH": "Expected step {{expected}}, received {{received}}",
"AUTH_TOKEN_REUSE": "Security alert: session was reused. Please log in again."
}
Backend Usage¶
All error codes are defined in the ErrorCode enum (src/common/constants/error-codes.ts). Services throw coded errors via AppException:
import { AppException, ErrorCode } from '../common';
import { HttpStatus } from '@nestjs/common';
// Simple — code alone is sufficient
throw new AppException(ErrorCode.AUTH_TOKEN_INVALID, 'Invalid token', HttpStatus.UNAUTHORIZED);
// With params — frontend interpolation
throw new AppException(ErrorCode.NOT_FOUND, 'Student not found', HttpStatus.NOT_FOUND, {
params: { entity: 'student' },
});
// With data — structured payload (import pipeline)
throw new AppException(ErrorCode.IMPORT_VALIDATION_FAILED, 'Import validation failed', HttpStatus.UNPROCESSABLE_ENTITY, {
data: { errors },
});
Naming convention: Use UPPER_SNAKE_CASE for all error codes. New codes must be added to the ErrorCode enum.
Generic Error Codes¶
| Code | Status | Params | Description |
|---|---|---|---|
INTERNAL_ERROR |
500 | — | Catch-all for unhandled server errors. Real error logged server-side, never leaked. |
VALIDATION_FAILED |
400 | — | Class-validator failures or custom field validation. See Validation Errors. |
RATE_LIMITED |
429 | — | Rate limit exceeded. Global: 10 req/60s. Login: 5 req/60s. |
HTTP_<status> |
varies | — | Fallback for standard NestJS HttpException subclasses without a custom code. |
Authentication Error Codes¶
| Code | Status | Params | When |
|---|---|---|---|
INVALID_CREDENTIALS |
401 | — | Email exists but password does not match. |
AUTH_TOKEN_INVALID |
401 | — | JWT expired, malformed, or revoked. Refresh token invalid/expired. User record not found. |
AUTH_TOKEN_REUSE |
401 | — | Refresh token replay detected — entire token family revoked. Frontend should force re-login. |
AUTH_REFRESH_MISSING |
401 | — | No refresh token cookie in request. |
AUTH_TENANT_INVALID |
401 | — | Invalid tenant selection during multi-tenant login. |
TENANT_SUSPENDED |
403 | — | Tenant is not active (suspended or trial-expired). |
USER_DEACTIVATED |
403 | — | User account is deactivated. |
Permission Error Codes¶
| Code | Status | Params | When |
|---|---|---|---|
INSUFFICIENT_SCOPE |
403 | — | User lacks the required scope+action for the route (ScopeGuard). |
ACTION_NOT_PERMITTED |
403 | — | User does not have the required action permission (ActionGuard). |
NO_AUTHENTICATED_USER |
403 | — | Permission guard ran without authenticated user. Configuration error. |
FORBIDDEN_FIELDS |
403 | — | Write request contains scope groups outside the user's writable scopes (FieldWriteGuard). |
Database Error Codes¶
| Code | Status | Params | When |
|---|---|---|---|
NOT_FOUND |
404 | { entity } |
Record not found. entity is the lowercased model/entity name (e.g. "student", "teacher"). |
CONFLICT |
409 | { entity } |
Unique constraint violation. entity from Prisma model name when available. |
FOREIGN_KEY_VIOLATION |
400 | — | Foreign key constraint failed. |
NULL_CONSTRAINT_VIOLATION |
400 | — | A required (non-nullable) field received null. |
REQUIRED_RELATION_VIOLATION |
400 | — | A required relation is missing. |
DATABASE_TIMEOUT |
503 | — | Database connection pool timeout. Clients should retry. |
DATABASE_ERROR |
400 | — | Unmapped Prisma error code. |
Setup Wizard Error Codes¶
| Code | Status | Params | When |
|---|---|---|---|
SETUP_STEP_MISMATCH |
409 | { expected, received } |
Frontend's currentStep doesn't match backend state. User should reload. |
SETUP_INVALID_NAVIGATION |
400 | — | Cannot navigate to target step (only backward, same, or next allowed). |
SETUP_DATA_REQUIRED |
400 | — | Data is required for forward navigation. |
SETUP_STEP_INCOMPLETE |
400 | { step } |
Step not complete, can't advance. |
SETUP_VALIDATION_FAILED |
400 | { reason, field? } |
Step-specific business rule validation failed. See reason values below. |
SETUP_VALIDATION_FAILED reason values¶
Frontend uses compound key SETUP_VALIDATION_FAILED.{reason} for translation:
reason |
When | Example field |
|---|---|---|
date_range_invalid |
startDate >= endDate | academicYear, periods |
period_out_of_bounds |
period outside year range | periods |
overlap |
overlapping periods of same type | periods |
duplicate_name |
non-unique name (case-insensitive) | departments, grades, periods |
ordinal_gap |
non-sequential ordinal positions | departments, grades |
missing_departments |
not all tenant depts represented | departments |
missing_year |
no academic year found | — |
invalid_department |
department not owned by tenant | departments |
Import Validation Error Codes¶
Top-level envelope uses IMPORT_VALIDATION_FAILED (422). Each error inside data.errors[] carries an ImportErrorCode for i18n.
{
"statusCode": 422,
"code": "IMPORT_VALIDATION_FAILED",
"message": "Import validation failed",
"data": {
"errors": [
{ "code": "FIELD_INVALID", "column": "gender", "rows": "2-5, 8", "allowedValues": ["MALE", "FEMALE"] },
{ "code": "HEADERS_MISSING", "column": "headers", "rows": "1", "params": ["last_name"], "allowedValues": ["first_name", "last_name"] }
]
}
}
| Code | Description |
|---|---|
HEADERS_MISSING |
Expected columns not found. params lists missing names. |
FIELD_REQUIRED |
A required field is empty. |
FIELD_MAX_LENGTH |
Value exceeds maximum length. |
FIELD_INVALID |
Value doesn't match allowed options. allowedValues lists valid options. |
Validation Errors¶
Produced by the global ValidationPipe (class-validator). Returns structured field-level errors:
{
"statusCode": 400,
"code": "VALIDATION_FAILED",
"message": "Validation failed",
"data": {
"errors": [
{ "field": "email", "rule": "isEmail" },
{ "field": "anagraphic.firstName", "rule": "isNotEmpty" },
{ "field": "anagraphic.firstName", "rule": "maxLength", "params": { "max": "100" } }
]
}
}
Each error has:
- field — dot-notation path to the field
- rule — class-validator constraint name (e.g. isEmail, isNotEmpty, maxLength, minLength)
- params — optional constraint parameters for interpolation (e.g. { max: "100" })
Frontend maps rule to a translated message: validation.isEmail → "Must be a valid email address".
Custom Fields Error Codes¶
Custom field validation errors use VALIDATION_FAILED with English messages. Custom field CRUD errors:
| Code | Status | When |
|---|---|---|
VALIDATION_FAILED |
400 | Wrong type, unknown field key, missing required field, invalid SELECT value, SELECT missing options, duplicate options. |
CONFLICT |
409 | Duplicate fieldKey within same tenant+entity. |
FORBIDDEN_FIELDS |
403 | User lacks WRITE access on target entity+scope. |
NOT_FOUND |
404 | Definition not found, or scope not found in permission catalogue. |
Typed Swagger Schemas¶
Swagger documents 7 distinct error response shapes. Each is a DTO class in src/common/dto/ — the base carries no params/data, subclasses add strongly typed fields.
| # | DTO | Codes | params type |
data type |
|---|---|---|---|---|
| 1 | ErrorResponseDto (base) |
20 simple codes (AUTH_*, RATE_LIMITED, INSUFFICIENT_SCOPE, etc.) | — | — |
| 2 | EntityErrorResponseDto |
NOT_FOUND, CONFLICT | EntityErrorParams { entity } |
— |
| 3 | StepMismatchErrorResponseDto |
SETUP_STEP_MISMATCH | StepMismatchErrorParams { expected, received } |
— |
| 4 | StepIncompleteErrorResponseDto |
SETUP_STEP_INCOMPLETE | StepIncompleteErrorParams { step } |
— |
| 5 | SetupValidationErrorResponseDto |
SETUP_VALIDATION_FAILED | SetupValidationErrorParams { reason, field? } |
— |
| 6 | ValidationErrorResponseDto |
VALIDATION_FAILED | — | ValidationErrorData { errors: ValidationFieldErrorDto[] } |
| 7 | ImportErrorResponseDto |
IMPORT_VALIDATION_FAILED | — | ImportValidationData { errors: ImportValidationErrorDto[] } |
Source files:
- Base + params: src/common/dto/error-response.dto.ts, src/common/dto/error-params.dto.ts
- Subclasses: src/common/dto/typed-error-responses.dto.ts
- Validation data: src/common/dto/validation-error-response.dto.ts, src/common/dto/import-validation-error.dto.ts
Runtime note: The AllExceptionsFilter continues to produce plain JSON objects. The typed DTOs are purely Swagger schema documentation — the filter's output already matches the DTO structures.