Skip to content

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.
  • field and 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.