Use this file to discover all available pages before exploring further.
All API responses follow a consistent structure. When a request fails, the response body always contains success: false alongside an error object describing what went wrong.
Returned when the request body or query string fails Zod schema validation. The details array maps directly to Zod’s error format, giving you the exact field path and reason.
INTERNAL_ERROR responses indicate an unexpected failure. Check your server logs for the full stack trace. The error middleware logs all request errors using Pino before sending the response.
The server uses a hierarchy of typed error classes. Throwing one of these anywhere in a route handler or middleware automatically produces the correct HTTP status and error code.
// src/utils/errors.tsexport class AppError extends Error { constructor( public readonly message: string, public readonly statusCode: number = 500, public readonly code: string = "INTERNAL_ERROR", public readonly isOperational: boolean = true ) { ... }}export class ValidationError extends AppError { constructor(message: string) { super(message, 400, "VALIDATION_ERROR"); }}export class UnauthorizedError extends AppError { constructor(message: string = "Unauthorized") { super(message, 401, "UNAUTHORIZED"); }}export class ForbiddenError extends AppError { constructor(message: string = "Forbidden") { super(message, 403, "FORBIDDEN"); }}export class NotFoundError extends AppError { constructor(message: string = "Resource not found") { super(message, 404, "NOT_FOUND"); }}export class ConflictError extends AppError { constructor(message: string) { super(message, 409, "CONFLICT"); }}
The global error handler in src/middleware/error.middleware.ts intercepts all thrown errors and maps them to the standard response shape:
Zod errors — caught by instanceof ZodError, serialized with field-level details.
App errors — caught by instanceof AppError, status and code taken from the class instance.
Fastify errors — framework-level errors (e.g. route not found, payload too large) returned with their native statusCode.
Always throw typed error classes in your route handlers rather than constructing raw response objects. This ensures every error is logged consistently and the response format never diverges from the standard shape.
Validation is applied at the route level using validateBody and validateQuery helpers. They parse the incoming data with a Zod schema and re-throw any ZodError, which the error middleware then formats automatically.
Use the ApiResponse type to model every response. Check the success flag before accessing data; otherwise read error.code to branch your error handling.
import type { ApiResponse } from "./types/api.types";interface LoginData { accessToken: string; user: { id: string; email: string };}type ErrorCode = | "VALIDATION_ERROR" | "UNAUTHORIZED" | "FORBIDDEN" | "NOT_FOUND" | "CONFLICT" | "INTERNAL_ERROR";async function login(email: string, password: string): Promise<LoginData> { const res = await fetch("/api/v1/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email, password }), credentials: "include", }); const body: ApiResponse<LoginData> = await res.json(); if (!body.success) { const code = body.error?.code as ErrorCode; switch (code) { case "VALIDATION_ERROR": // Surface field-level messages from body.error.details throw new Error(`Invalid input: ${body.error?.message}`); case "UNAUTHORIZED": throw new Error("Invalid email or password."); case "CONFLICT": throw new Error("An account with this email already exists."); default: throw new Error(body.error?.message ?? "An unexpected error occurred."); } } return body.data!;}