An API error contract is a stable JSON response shape returned for every failed request, produced here by mapping thrown HTTP exceptions through a single global exception filter.
In simpler words
No matter what breaks, clients get the same kind of error object back — so the frontend can show a message without guessing the shape each time.
Ground in AllExceptionsFilter and ApiErrorBody from @repo/contracts.
Explain how correlationId connects a client error to server logs
Choose the HTTP exception that matches a failure
One filter, one shape
Definition
AllExceptionsFilter is a global, catch-all Nest exception filter that inspects any thrown exception, derives a status, error label, message, and optional field-level details, and serializes an ApiErrorBody with a correlationId sourced from the request.
In simpler words
It does not decide business rules — it only standardizes how a thrown exception becomes a response body.
HttpException subclasses — NotFoundException, ForbiddenException, BadRequestException from ValidationPipe, and others — map their status and message straight into the shape.
An unexpected non-HTTP error becomes a generic 500 with a safe message; the real error is logged server-side, never sent to the client.
ApiErrorBody
{
"statusCode": 400,
"error": "Bad Request",
"message": ["title must be longer than or equal to 1 characters"],
"details": [{ "field": "request", "message": "..." }],
"correlationId": "..."
}
message is always a string or string[]; the frontend never has to branch on a fifth shape.
Throw the exception that matches the failure
Definition
Choosing an HTTP exception class is choosing the contract a client will see, so a thrown exception should represent the actual failure — not-found, validation, authorization — rather than a generic error.
In simpler words
A missing ticket, an invalid role, and a bad body are different failures and should produce different statuses.
TicketsService.get and .remove throw NotFoundException when a row does not exist — the controller stays free of if-not-found branching.
Never catch and swallow an exception just to return 200 with an error flag in the body; that breaks the contract for every consumer.
Service throws, filter formats
async get(id: string) {
const ticket = await this.ticketsRepo.findOne({ where: { id } });
if (!ticket) throw new NotFoundException('Ticket not found');
return this.toResponse(ticket);
}
The filter turns this into { statusCode: 404, error: "Not Found", message: "Ticket not found", correlationId }.
Keep in mind
Let the exception filter be the only place that builds an error response.
Pick 404/400/401/403 by what actually failed, not by convenience.
Never leak stack traces or raw driver errors to clients in production.
Test
Check your understanding
At least 10 questions — mix of concept, syntax, practical, and logic. Score ≥ 80% (enforced by the API) to save progress.