Frontend error contract¶
Render API errors from messages[lang]. The backend owns localized error text; the client uses codes and locators for behavior.
1. Response shape¶
Errors handled by the API exception filter have this shape:
{
"statusCode": 409,
"code": "ROOM_TYPE_IN_USE",
"messages": {
"en_US": "This room type is used by 3 rooms and cannot be deleted",
"it_IT": "Questo tipo di aula è utilizzato da 3 aule e non può essere eliminato"
},
"timestamp": "2026-01-01T10:00:00.000Z",
"path": "/api/v1/room-types/abc"
}
Optional field identifies the request-body-relative input. Optional data is emitted only for the collection codes registered in COLLECTION_ENRICHMENTS. Neither message nor params is part of the wire contract.
| Field | Purpose |
|---|---|
messages |
Fully rendered display text in en_US and it_IT |
code |
Stable behavior hook, such as refresh or retry handling |
field |
Optional dotted body path, including numeric array positions |
data |
Optional message-bearing collection and explicitly supported context |
statusCode |
HTTP status |
timestamp, path |
Request context; sensitive query parameters are scrubbed |
The filter and renderer are the backend authorities:
src/common/filters/all-exceptions.filter.ts and
src/common/i18n/render-error-messages.ts.
Error handling explains how backend code produces this contract.
2. Language and display¶
Resolve a supported language once for the application:
type Lang = 'en_US' | 'it_IT';
function resolveLang(userLocale: string): Lang {
return userLocale.toLowerCase().startsWith('it') ? 'it_IT' : 'en_US';
}
// After validating the API error envelope:
toast(error.messages[lang]);
Do not construct copy from code, interpolate backend parameters, or translate enum values inside a rendered message. Names and user-authored values remain their original text. An unresolved template placeholder is a backend error, not a client interpolation instruction.
3. Collections¶
The common validation collections reduce each leaf to { field?, messages }. Internal rule names, import column/row discriminators, allowed-value arrays and prerequisite identifiers are rendered into the text; they are not extra client fields.
| Code | Collection |
|---|---|
VALIDATION_FAILED |
data.errors |
IMPORT_VALIDATION_FAILED |
data.errors |
SELECTION_VALIDATION_FAILED |
data.violations |
SELECTION_WINDOW_PREREQ_FAILED |
data.gaps |
For forms, place each error by its field when present:
for (const leaf of error.data?.errors ?? []) {
if (leaf.field) {
formApi.setFieldError(leaf.field, leaf.messages[lang]);
}
}
Fields are relative to the submitted body, such as identity.firstName or
configuration.maximumCapacity. The backend renders localized field and container labels into the message. Do not prepend the raw path to user-facing text.
Some collections carry pre-rendered leaves with additional structured context:
| Code | Collection and context |
|---|---|
MAILING_GROUP_MEMBER_NOT_CONTACTABLE |
data.rejected: supplied personType/personId references and messages |
TIMETABLE_GENERATION_PRECHECK_FAILED |
data.violations: diagnostic cards; optional skippedSubjectGroups |
TIMETABLE_GENERATION_INFEASIBLE |
Diagnostic cards plus optional skippedSubjectGroups and solveMetadata |
TIMETABLE_GENERATION_TIMEOUT |
Same context carrier; violations may be empty |
Use the owning domain DTO for diagnostic-card details. Do not infer an arbitrary data payload from a backend throw's internal parameters.
For every registered collection, the envelope message composes up to three leaf messages, followed by a localized count of remaining errors. An empty collection uses the code's general message. A client that only shows the envelope still receives useful text.
A generic list renderer can cover all registered arrays:
function errorLines(error: ApiError, lang: Lang): string[] {
const leaves = error.data?.errors
?? error.data?.violations
?? error.data?.gaps
?? error.data?.rejected;
return leaves?.length
? leaves.map((leaf) => leaf.messages[lang])
: [error.messages[lang]];
}
4. Behavioral handling¶
Use a small shared set of transport/flow reactions:
AUTH_TOKEN_EXPIRED: attempt the application's supported refresh flow and retry once.- Invalid, missing or reused authentication: return to authentication according to the session flow.
RATE_LIMITED/ HTTP 429: respect retry guidance and back off. Limits vary by endpoint; do not encode request quotas in display logic.- Server or network failure: offer retry where repeating the operation is safe.
fieldand explicitly documented collection identifiers: focus inputs or highlight the supplied entries.
A domain conflict usually needs its rendered message and a refreshed view. Do not build a per-domain-code translation table.
5. Client types¶
export type Lang = 'en_US' | 'it_IT';
export interface LocalizedMessage {
en_US: string;
it_IT: string;
}
export interface ErrorLeaf {
field?: string;
messages: LocalizedMessage;
[key: string]: unknown;
}
export interface ApiError {
statusCode: number;
code: string;
messages: LocalizedMessage;
field?: string;
data?: {
errors?: ErrorLeaf[];
violations?: ErrorLeaf[];
gaps?: ErrorLeaf[];
rejected?: ErrorLeaf[];
[key: string]: unknown;
};
timestamp: string;
path: string;
}
Narrow more specific collection types when a screen consumes domain context. Validate untrusted response data before treating it as ApiError.
6. Transport boundaries¶
A network failure, proxy response, timeout or malformed body may not have the API envelope. Keep one client-owned localized fallback for that case; the existence of a JSON body alone does not prove it is an API error.
Framework exceptions handled by the API filter still receive messages. Unmapped framework errors may use HTTP_<status> and the same fallback text in both language keys. Display the supplied text and report missing localization through the backend catalogue.
The catalogue in src/common/i18n/error-messages.catalog.ts owns error copy; startup coverage and drift specifications guard its completeness. Client code depends on the response contract.