Fullstack CourseLearn by building
Back to week 3

Topic

Error contracts / HTTP exceptions → stable JSON

Definition

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.

Center this on a global exception filter (an AllExceptionsFilter) and a shared ApiErrorBody error shape.

After this you can

  • Read the shared ApiErrorBody shape
  • 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.

Checking your session…

15 questions · concept 5 · syntax 3 · practical 4 · logic 3

1. How do errors become stable JSON here?
Concept
2. Why a single filter?
Syntax
3. Should 500 responses expose stack traces to browsers?
Practical
4. What is correlationId for?
Logic
5. Who should build error JSON?
Concept
6. What does NotFoundException become?
Practical
7. What does UnauthorizedException become?
Syntax
8. What does ForbiddenException become?
Logic
9. Why include an error field besides message?
Concept
10. Can filters catch errors from guards and pipes?
Practical
11. What response does the client get from this code?
Conceptintermediate
const ticket = await this.repo.findOneBy({ id });
if (!ticket) throw new NotFoundException('Ticket not found');
12. What is wrong with this error response?
Syntaxadvanced
catch (e) {
  return res.status(500).json({ message: e.message, stack: e.stack });
}
13. Why are these two controllers a problem?
Practicalintermediate
// controller A
return res.json({ err: 'bad' });
// controller B
return res.json({ error: { message: 'bad' } });
14. Does this filter also format errors thrown by guards and pipes?
Logicadvanced
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  catch(ex, host) { /* build ApiErrorBody */ }
}
15. What statusCode appears in the error body for this throw?
Conceptintermediate
throw new ForbiddenException('Admins only');

Checking your session…