Validating data with a schema
A client can send anything: an empty title, a number instead of text, extra fields. Fastify checks incoming data against a schema — a description in the JSON Schema format (a standard where a data structure is defined with a plain object). If the data does not match, the handler is not even called, and the client gets a 400 error.
A body schema
The schema is passed as the second route argument in the schema option. In src/routes/posts.js:
// src/routes/posts.js
const createPostSchema = {
body: {
type: 'object',
required: ['title', 'body'],
properties: {
title: { type: 'string', minLength: 3, maxLength: 120 },
body: { type: 'string', minLength: 1 },
},
},
};
app.post('/posts', { schema: createPostSchema }, async (request, reply) => {
const post = { id: posts.length + 1, ...request.body };
posts.push(post);
return reply.code(201).send(post);
});The schema requires two string fields: title of 3 to 120 characters and a non-empty body.
What the client gets
Send a post without text:
curl -X POST http://localhost:3000/posts \
-H "Content-Type: application/json" \
-d '{"title":"Hello"}'The server answers with code 400:
{
"statusCode": 400,
"code": "FST_ERR_VALIDATION",
"error": "Bad Request",
"message": "body must have required property 'body'"
}A request with both fields passes and returns the new post with code 201.
Params and query
The same rules work for params and querystring (that is the name of the query string schema):
// src/routes/posts.js
const listSchema = {
querystring: {
type: 'object',
properties: {
limit: { type: 'integer', minimum: 1, maximum: 50, default: 10 },
},
},
};
const showSchema = {
params: {
type: 'object',
properties: {
id: { type: 'integer' },
},
},
};
app.get('/posts', { schema: listSchema }, async (request) => {
return posts.slice(0, request.query.limit);
});
app.get('/posts/:id', { schema: showSchema }, async (request) => {
return posts.find((post) => post.id === request.params.id);
});Fastify converts strings to numbers by the integer type, so Number() is no longer needed. default: 10 is used when limit is missing, and the address /posts/abc returns a 400 error.
Main keywords
| Keyword | Example | Meaning |
|---|---|---|
type | 'string', 'integer', 'object' | Value type |
required | ['title'] | Required fields |
minLength, maxLength | 3 | String length |
minimum, maximum | 1 | Number bounds |
enum | ['draft', 'published'] | Allowed values |
format | 'email' | String format |
default | 10 | Default value |
Common mistake
In Fastify 5 a schema must be complete. The old shorthand without type: 'object' and properties is no longer supported: the server either fails to start or the check silently does nothing:
// src/routes/posts.js
// Wrong in Fastify 5:
const schema = { querystring: { limit: { type: 'integer' } } };Always write type: 'object' and list the fields in properties.