Fastify Schemas as Contracts: Validation, Serialization, and Plugin Encapsulation
Use JSON Schema as the contract in Fastify for automatic request validation with AJV and pre-compiled response serialization with fast-json-stringify, scoped inside encapsulated plugins for modular, type-safe routes.
24 Jan 2026, 17:25 UTC

APIs drift when validation lives in middleware and serialization is ad-hoc. Fastify treats a JSON Schema as the contract for a route and uses it for both input validation and output serialization. That removes repetitive checks and lets the framework pre-compile serializers.
The practical takeaway is to define strict input and response schemas per route and keep them inside encapsulated plugins. You get consistent 400 validation errors without custom middleware, and fast-json-stringify can serialize known shapes with less runtime allocation than generic JSON.stringify.
Schemas drive validation and serialization
When a route option includes schema for body, params, querystring or headers, Fastify validates incoming data automatically with AJV, Another JSON Schema Validator. No manual checks are required. Invalid requests return a 400 response with a standardized error shape and a details array exposing keyword and path.
Response schemas do a different job. If you provide a response schema, Fastify uses fast-json-stringify to pre-compile a serializer at startup for that shape. The serializer is built once per route and reused, which avoids the allocation cost of generic stringify for known outputs.
Encapsulation keeps contracts local
Fastify's plugin system creates encapsulation contexts. Routes, hooks, schemas and decorators registered inside a plugin are scoped to that context and do not leak to the parent or sibling plugins.
This matters for schema-driven services. A plugin can define its own decorators and schemas without polluting the global instance. You can verify scope by defining a decorator inside a plugin and checking it is accessible in routes registered within the plugin but not on the root instance after registration.
Worked example: strict user route in a plugin
The pattern is a strict input schema with additionalProperties false, required fields, and a matching response schema passed in the route options.
// users-plugin.js
export default async function usersPlugin(fastify, opts) {
fastify.decorate('db', opts.db)
fastify.get('/users/:id', {
schema: {
params: {
type: 'object',
properties: { id: { type: 'string', pattern: '^[0-9a-f]{24}$' } },
required: ['id'],
additionalProperties: false
},
response: {
200: {
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' }
},
required: ['id', 'name'],
additionalProperties: false
}
}
}
}, async (request, reply) => {
const user = await fastify.db.findById(request.params.id)
return { id: user.id, name: user.name }
})
}
Register the plugin from the root app with fastify.register(usersPlugin, { db }). The params schema rejects malformed ids before your handler runs. The 200 response schema lets Fastify pre-compile a serializer for the known shape.
To check behavior, create a minimal app, register a GET route with a params schema, and call it with an invalid param type. Expect a 400 response with validation details. Register a route with a response schema and compare the serialized output shape against the schema definition to confirm the route uses the compiled serializer.
Trade-offs and limits
Schema compilation happens at startup and per route. Many large or complex schemas increase boot time and memory usage.
fast-json-stringify requires a stable, fully described schema. Dynamic keys, unknown unions, or runtime-generated shapes may fall back to generic serialization or require a custom serializer.
AJV options, error formatting and supported schema keywords are version sensitive across Fastify major versions. Validation semantics can shift between releases, so pin versions and review changelogs when upgrading.
Use schemas where the shape is stable and shared, keep them small and strict, and isolate them in plugins so changes are bounded. That gives you consistent validation, faster serialization for known responses, and modular contracts without leaking implementation across the service.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.