Fastify JSON Schemas as Executable API Contracts
Fastify compiles route-level JSON Schemas into validators and serializers. Here is how to use them as executable contracts, what breaks when schemas drift, and how to verify the behavior.
21 Aug 2025, 23:31 UTC

The repetitive validation tax
If you have built HTTP APIs in Node.js, you have probably written the same guard clauses dozens of times: check that items is an array, verify total is a positive number, and manually strip internal fields before sending a response. These checks are easy to forget, hard to test uniformly, and they push contract enforcement into imperative code that drifts from documentation.
Fastify offers a different model. You attach a JSON Schema object directly to route options, and the framework compiles it into fast validators and serializers. The useful takeaway is that your route definition becomes an executable contract: malformed requests are rejected before your handler runs, and responses are shaped automatically without manual JSON.stringify calls or field-by-field copying.
How schema compilation works
Fastify does not interpret schemas at runtime on every request. When the server boots, it reads the schema property from your route configuration and passes it to two underlying libraries: Ajv for request validation and fast-json-stringify for response serialization. Both generate optimized JavaScript functions ahead of time.
This matters for two reasons. First, validation happens at the framework boundary, so your handler receives data that already matches the declared shape. Second, serialization is not a generic JSON.stringify; it is a compiled function that knows exactly which fields to emit. Behavior follows the JSON Schema dialect supported by the bundled Ajv and fast-json-stringify versions, so pin those dependencies and test edge cases such as additionalProperties, nullable fields, and custom formats rather than assuming full draft compliance.
A worked example: POST /orders
Consider an order endpoint. The contract requires a string id, an items array, and a numeric total. The 200 response should echo those fields plus a status string.
// server.js
const fastify = require('fastify')({ logger: true })
const orderBodySchema = {
type: 'object',
required: ['id', 'items', 'total'],
properties: {
id: { type: 'string' },
items: {
type: 'array',
items: { type: 'object' }
},
total: { type: 'number' }
},
additionalProperties: false
}
const orderResponseSchema = {
type: 'object',
properties: {
id: { type: 'string' },
items: { type: 'array' },
total: { type: 'number' },
status: { type: 'string' }
}
}
fastify.post('/orders', {
schema: {
body: orderBodySchema,
response: {
200: orderResponseSchema
}
}
}, async (request, reply) => {
const { id, items, total } = request.body
return { id, items, total, status: 'received' }
})
fastify.listen({ port: 3000 }, (err) => {
if (err) process.exit(1)
})
Run this in a terminal with node server.js. No elevated permissions are required beyond standard file access.
Send a valid payload:
curl -X POST http://localhost:3000/orders \
-H 'Content-Type: application/json' \
-d '{"id":"ord-1","items":[{"sku":"A1"}],"total":25.50}'
You should receive a 200 response containing id, items, total, and status. Now send an invalid payload that omits total or sends it as a string. Fastify returns a 400 status with a structured error body before your handler executes.
To see the stripping behavior, temporarily add an extra field to the handler return value, such as internalNote: 'debug'. Because internalNote is absent from orderResponseSchema, the serialized response will not include it. This is fast-json-stringify omitting undeclared properties, not an error.
The maintenance and drift trade-off
Inline schemas are convenient for a single route, but they duplicate quickly. If ten endpoints accept the same address object, copying that schema ten times creates a maintenance burden: a change in one place must be mirrored in nine others.
Fastify's addSchema method addresses this. You register reusable components with an $id and reference them using $ref:
fastify.addSchema({
$id: 'address',
type: 'object',
properties: {
street: { type: 'string' },
city: { type: 'string' }
}
})
Then use { $ref: 'address' } inside route schemas. This keeps contracts in one module and reduces drift.
A related limitation is silent response stripping. If a handler returns a field that clients depend on but the response schema omits it, the client will not see the field and no error is raised. For highly dynamic or polymorphic payloads, schemas can become verbose, and the performance benefit of compiled serializers may shrink because the generated function handles many conditional shapes. Benchmark critical paths rather than assuming schemas are always faster than hand-written serialization.
Verifying the behavior
To confirm the contract is active, inspect validation errors by temporarily enabling Ajv's allErrors option in the Fastify constructor:
const fastify = require('fastify')({
logger: true,
ajv: {
customOptions: { allErrors: true }
}
})
Restart the server and resend the invalid payload. The 400 response should list every validation failure, not just the first one.
For performance, run a simple load test against the same route with and without the schema option. A tool like autocannon can quantify the overhead in your environment. If latency on a high-throughput route increases more than your budget allows, consider narrowing the schema to only the fields that truly need enforcement.
Finally, compare the serialized output against a raw JSON.stringify of the same object. Any fields present in the raw output but missing from the Fastify response are being stripped by the serializer. That comparison is the fastest way to detect schema drift.
Actionable takeaway
Treat route schemas as part of your API's source of truth, not as optional documentation. Start with request validation to reject bad data early, add response schemas to lock down output shapes, and extract shared definitions with addSchema before duplication sets in. Pin your Ajv and fast-json-stringify versions, and verify stripping and latency in your own environment before relying on schemas for critical paths.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.