Skip to content

Logging and errors

The API logs one JSON line per event with a request id, every error response has the same shape with a stable code, and the client turns those codes into translated messages. Read this when you debug a failed request in production, or when you add an error of your own.

Logging

The API logs through pino via @core/api-logging. Every Nest Logger call goes there too.

  • Format: one JSON object per line on stdout (LOG_FORMAT=json, the default outside development), or readable single lines (pretty, needs the pino-pretty dev dependency).
  • Request id: each request gets an id, taken from a valid incoming X-Request-Id header ([A-Za-z0-9._-], up to 128 characters) or generated. It is returned in the X-Request-Id response header and appears as reqId on every line logged while handling that request.
  • Access log: one line per request with method, path (no query string), status, response time and, when signed in, userId. Health checks log at debug; 4xx at warn; 5xx at error.
  • Never logged: request or response headers and bodies (so no tokens, cookies or passwords), query strings, or email addresses.

Errors

Every error response has the same body (ApiError in @core/shared):

{ "statusCode": 404, "code": "NOT_FOUND", "message": "Todo #1 not found",
"details": { ... }, "requestId": "9b2f…" }
  • code is stable and meant for programs; message is for people.
  • requestId equals the X-Request-Id header and the reqId in the logs.
  • details.fields lists each failed validation rule as { field, message } (VALIDATION_FAILED); details.requiredPermission names the missing permission (PERMISSION_REQUIRED).
  • 5xx bodies are always INTERNAL_ERROR / “Internal server error”; the cause is only in the server log.

Every code, its status and its meaning: Error codes.

To return a specific code from a service, put it in the exception body: throw new ForbiddenException({ message: '…', code: 'EMAIL_NOT_VERIFIED' }). Exceptions thrown with a plain message get the default code for their status.

Errors in the client

A failed request shows up once, as a toast in the bottom-right corner (spartan’s sonner, hlm-toaster in both layouts, 5 s, closable, follows light/dark). errorToastInterceptor (@core/client-i18n, registered by provideCore inside authInterceptor, so a 401 is refreshed and retried first) maps the body’s code - else the status: 403, 404, 409, 413, 415, 429, 5xx, 0 = offline/unreachable - to a translated key with httpErrorKey, the same function the pages use. The server’s message is never shown. The same text twice within 3 s makes one toast. The error still reaches the caller, so it can revert an optimistic change.

No toast for a 401, a VALIDATION_FAILED with details.fields (the form shows them), or a request marked SKIP_ERROR_TOAST.

Add an error code: add it to ERROR_CODES in @core/shared; the client does not compile until it has a key in CODE_KEYS (libs/core/client/i18n/src/lib/utils/http-error-key.ts); add that key to both en.json and el.json under errors (a spec checks every key httpErrorKey can return exists in both). A message that depends on the endpoint (a 400 on “resend” = sent too recently) goes in httpErrorKey under an ErrorContext, and the data-access method sets it: { context: errorContext('resendInvitation') }.

Opt out where the screen shows the failure itself - a list with a Retry banner, a dialog that stays open with its inline error, an auth page, a background poll: { context: skipErrorToast() } on the request (or new HttpContext().set(SKIP_ERROR_TOAST, true)). A failure the interceptor never sees - a client-side timeout() - or a client-only message goes through ErrorToast (showClientError(error, context), show(key)).