How to Implement Request Validation and Centralized Error Handling in Fastify 4 Using TypeBox and TypeScript

User avatar placeholder
Written by Tamzid Ahmed

October 13, 2026

Building reliable APIs requires more than just routing logic — it demands rigorous input validation and consistent error responses. Fastify 4, combined with TypeBox and TypeScript, provides a powerful foundation for achieving both with minimal boilerplate.

Why Request Validation and Centralized Error Handling Matter in Fastify 4

Fastify’s performance-first architecture makes it a top choice for high-throughput Node.js services. However, without proper validation, malformed requests can cause runtime crashes or security vulnerabilities. Centralized error handling ensures that every failure — whether a validation error, database issue, or unexpected exception — returns a consistent, machine-readable response. This approach reduces debugging time and improves client integration.

TypeBox solves a critical gap: it generates JSON Schema definitions directly from TypeScript types, eliminating the duplication between runtime validation schemas and compile-time type definitions. When paired with Fastify’s native schema compilation, you get both performance and type safety.

Setting Up Fastify 4 with TypeBox and TypeScript

Start by initializing a new project with the required dependencies. Use fastify@4, @sinclair/typebox, and @fastify/type-provider-typebox for seamless integration. Install TypeScript and type definitions as dev dependencies.

npm init -y
npm install fastify@4 @sinclair/typebox @fastify/type-provider-typebox
npm install -D typescript @types/node tsx

Configure tsconfig.json with "moduleResolution": "bundler" and "target": "ES2022" to support modern ESM imports. Create a src directory and add an entry point server.ts.

Defining Schemas with TypeBox for Type-Safe Validation

TypeBox uses a fluent API to define schemas that are both valid JSON Schema and TypeScript types. Import Type from @sinclair/typebox and define your request bodies, query parameters, and response shapes.

import { Type } from '@sinclair/typebox'

export const CreateUserBody = Type.Object({
  username: Type.String({ minLength: 3, maxLength: 20 }),
  email: Type.String({ format: 'email' }),
  age: Type.Optional(Type.Integer({ minimum: 13, maximum: 120 }))
})

export type CreateUserBodyType = Static

The Static utility extracts the inferred TypeScript type, enabling full IntelliSense and compile-time checking in route handlers. Use Type.Optional for optional fields and Type.Array for lists. This schema becomes the single source of truth for validation and documentation.

Registering the TypeBox Type Provider

Fastify’s type provider system allows TypeBox schemas to drive automatic request parsing and response serialization. Register it before defining routes:

import Fastify from 'fastify'
import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'

const app = Fastify().withTypeProvider()

This enables type-safe route definitions where the schema property directly accepts TypeBox schemas.

Implementing Centralized Error Handling with Fastify’s Error Hooks

Fastify provides two key hooks for error management: setErrorHandler for custom error formatting and addHook('onError') for logging or side effects. Combine both to create a unified error response format.

import { FastifyError, FastifyReply, FastifyRequest } from 'fastify'

app.setErrorHandler((error: FastifyError, request: FastifyRequest, reply: FastifyReply) => {
  const statusCode = error.statusCode ?? 500
  
  if (error.validation) {
    return reply.status(400).send({
      error: 'Validation Error',
      message: 'Request validation failed',
      details: error.validation
    })
  }

  request.log.error(error)
  
  return reply.status(statusCode).send({
    error: error.name ?? 'Internal Server Error',
    message: statusCode === 500 ? 'An unexpected error occurred' : error.message
  })
})

This handler distinguishes validation errors (which include error.validation) from application errors. It logs server errors while returning sanitized messages to clients. For production, integrate with a logging framework like Pino (Fastify’s default) and an APM tool.

Creating Custom Error Classes for Business Logic

Define domain-specific error classes that carry HTTP status codes and structured metadata. This allows throw new NotFoundError('User') in handlers while the centralized handler formats them correctly.

export class AppError extends Error {
  constructor(
    public readonly statusCode: number,
    public readonly code: string,
    message: string,
    public readonly details?: Record
  ) {
    super(message)
    this.name = this.constructor.name
  }
}

export class NotFoundError extends AppError {
  constructor(resource: string) {
    super(404, 'NOT_FOUND', `${resource} not found`)
  }
}

export class ConflictError extends AppError {
  constructor(message: string, details?: Record) {
    super(409, 'CONFLICT', message, details)
  }
}

Throw these in route handlers: if (!user) throw new NotFoundError('User'). The error handler automatically picks up statusCode and code for consistent responses.

Putting It All Together: A Complete Example

The following example demonstrates a complete POST /users endpoint with validation, business logic, and error handling. Notice how TypeBox schemas define both the contract and the types.

app.post('/users', {
  schema: {
    body: CreateUserBody,
    response: {
      201: Type.Object({
        id: Type.String({ format: 'uuid' }),
        ...CreateUserBody.properties
      }),
      400: Type.Object({
        error: Type.String(),
        message: Type.String(),
        details: Type.Array(Type.Object({}))
      })
    }
  }
}, async (request, reply) => {
  const { username, email, age } = request.body
  
  const existing = await db.users.findByEmail(email)
  if (existing) throw new ConflictError('Email already registered', { field: 'email' })
  
  const user = await db.users.create({ username, email, age })
  return reply.status(201).send(user)
})

The schema object enables Fastify’s compiler to generate optimized validation code at startup. Invalid requests never reach the handler. Successful responses are serialized against the 201 schema.

Testing Validation and Error Responses

Use a structured testing approach to verify behavior. The following checklist covers critical scenarios:

  • Valid request returns 201 with correct response shape
  • Missing required field returns 400 with validation details
  • Invalid email format returns 400 with field-specific message
  • Duplicate email returns 409 with conflict code and details
  • Database failure returns 500 with generic message (no stack trace)

Tools like @fastify/vite or supertest with vitest can automate these checks in CI pipelines.

Conclusion

Implementing request validation and centralized error handling in Fastify 4 with TypeBox and TypeScript creates a robust, type-safe API foundation. By defining schemas once with TypeBox, you eliminate drift between validation logic and TypeScript types. Fastify’s compiled schema validation delivers exceptional performance, while a single error handler ensures consistent, secure responses across all failure modes. Start by adding the TypeBox type provider to your Fastify instance, then migrate one route at a time — your future self will thank you when debugging production issues becomes straightforward.

Leave a Comment