Validate Fastify Requests and Responses with JSON Schema
Use Fastify's route-level JSON Schema to reject bad requests early and serialize responses safely, with plugin encapsulation and production trade-offs.
11 May 2026, 23:40 UTC

The problem: bad requests reach your handler
A Fastify service can still accept malformed payloads if every handler performs its own checks. Missing fields, wrong types, and out-of-range values slip through to business logic, where they surface as confusing errors or silent data corruption. Frontend teams then receive inconsistent response shapes, and debugging starts in the wrong layer.
The useful takeaway is to declare validation and serialization rules in the route options. Fastify compiles them once with Ajv at startup, rejects invalid requests with a standard 400 before the handler runs, and can optionally validate outgoing responses against a declared contract.
Declare a route schema and enforce it at the edge
JSON Schema describes the shape of request parameters, query strings, headers, and bodies. Ajv is the validator Fastify uses to compile those schemas into fast JavaScript functions. Because compilation happens when the server starts, per-request overhead stays low.
This example defines a POST route that accepts an order ID in the URL and a JSON body. It also declares a response schema for successful replies.
const fastify = require('fastify')({ logger: true })
fastify.post('/orders/:orderId', {
schema: {
params: {
type: 'object',
properties: {
orderId: { type: 'string', pattern: '^[0-9a-f-]{36}$' }
},
required: ['orderId']
},
body: {
type: 'object',
properties: {
item: { type: 'string' },
quantity: { type: 'integer', minimum: 1 }
},
required: ['item', 'quantity']
},
response: {
200: {
type: 'object',
properties: {
id: { type: 'string' },
status: { type: 'string' }
}
}
}
}
}, async (request, reply) => {
return { id: request.params.orderId, status: 'accepted' }
})
fastify.listen({ port: 3000 }, (err) => {
if (err) throw err
})
Run this file from your project directory with Node.js installed. You do not need root permissions unless you bind to a port below 1024. The schema is compiled when the process starts; if it is malformed, Fastify exits with an Ajv error before listening.
Test the route with curl. A valid request:
curl -X POST http://localhost:3000/orders/550e8400-e29b-41d4-a716-446655440000 \
-H 'Content-Type: application/json' \
-d '{"item":"widget","quantity":2}'
Expected check: a 200 response containing {"id":"...","status":"accepted"}. An invalid request, such as quantity set to 0:
curl -X POST http://localhost:3000/orders/550e8400-e29b-41d4-a716-446655440000 \
-H 'Content-Type: application/json' \
-d '{"item":"widget","quantity":0}'
Expected check: a 400 response with a structured validation message, and the handler never executes. If you change the schema and restart, Fastify recompiles it; a syntax error in the schema prevents startup, which is a useful early warning in CI.
Plugin encapsulation keeps schemas scoped
Fastify plugins create a boundary. Routes, decorators, and schemas registered inside a plugin are invisible outside it unless you explicitly opt out. This prevents naming collisions in large applications.
const fp = require('fastify-plugin')
async function ordersPlugin(fastify, options) {
fastify.decorate('orderService', { create: () => {} })
fastify.post('/orders', {
schema: {
body: { type: 'object', properties: { item: { type: 'string' } }, required: ['item'] }
}
}, async (request, reply) => {
return { received: request.body.item }
})
}
fastify.register(ordersPlugin, { prefix: '/api' })
// fastify.orderService and POST /api/orders are scoped to this plugin
To share a route or decorator across the whole application, wrap the plugin with fastify-plugin. Otherwise, code outside the register cannot access it, which is usually what you want for domain-specific validation rules.
Production trade-offs and how to verify
Response schema validation is opt-in. It improves contract safety by catching mismatched return shapes, but it adds CPU cost on every response. Many high-throughput services validate requests only and enable response validation per-route or in staging first.
Malformed JSON Schema causes startup failure. Cover schemas with unit and integration tests, and start the server in CI to catch compilation errors before deployment. Fastify v5 adjusted internals and plugin patterns, so verify migration steps if you are moving from v3 or v4.
Practical verification checklist:
- Send a valid payload and assert a 200.
- Send an invalid payload and assert a 400 with validation details.
- Start the server with an intentionally broken schema and confirm it exits before listening.
- Register a plugin with a route and decorator, then confirm they are inaccessible outside the register unless wrapped with fastify-plugin.
Closing
Start with request schemas on your mutating endpoints. Add response schemas where contract drift causes the most incidents, measure latency, and keep schemas under test. Fastify moves validation to the edge so your handlers can assume the input is already well formed.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.