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 thepino-prettydev dependency). - Request id: each request gets an id, taken from a valid incoming
X-Request-Idheader ([A-Za-z0-9._-], up to 128 characters) or generated. It is returned in theX-Request-Idresponse header and appears asreqIdon 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 atdebug; 4xx atwarn; 5xx aterror. - 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…" }codeis stable and meant for programs;messageis for people.requestIdequals theX-Request-Idheader and thereqIdin the logs.details.fieldslists each failed validation rule as{ field, message }(VALIDATION_FAILED);details.requiredPermissionnames 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)).