Fastify best practices skill
93
97%
Does it follow best practices?
Impact
85%
1.37xAverage score across 4 eval scenarios
Passed
No known issues
Prefer TypeBox for defining schemas. It provides TypeScript types automatically and compiles to JSON Schema:
import Fastify from 'fastify';
import { Type, type Static } from '@sinclair/typebox';
const app = Fastify();
// Define schema with TypeBox - get TypeScript types for free
const CreateUserBody = Type.Object({
name: Type.String({ minLength: 1, maxLength: 100 }),
email: Type.String({ format: 'email' }),
age: Type.Optional(Type.Integer({ minimum: 0, maximum: 150 })),
});
const UserResponse = Type.Object({
id: Type.String({ format: 'uuid' }),
name: Type.String(),
email: Type.String(),
createdAt: Type.String({ format: 'date-time' }),
});
// TypeScript types are derived automatically
type CreateUserBodyType = Static<typeof CreateUserBody>;
type UserResponseType = Static<typeof UserResponse>;
app.post<{
Body: CreateUserBodyType;
Reply: UserResponseType;
}>('/users', {
schema: {
body: CreateUserBody,
response: {
201: UserResponse,
},
},
}, async (request, reply) => {
// request.body is fully typed as CreateUserBodyType
const user = await createUser(request.body);
reply.code(201);
return user;
});import { Type, type Static } from '@sinclair/typebox';
// Enums
const Status = Type.Union([
Type.Literal('active'),
Type.Literal('inactive'),
Type.Literal('pending'),
]);
// Arrays
const Tags = Type.Array(Type.String(), { minItems: 1, maxItems: 10 });
// Nested objects
const Address = Type.Object({
street: Type.String(),
city: Type.String(),
country: Type.String(),
zip: Type.Optional(Type.String()),
});
// References (reusable schemas)
const User = Type.Object({
id: Type.String({ format: 'uuid' }),
name: Type.String(),
address: Address,
tags: Tags,
status: Status,
});
// Nullable
const NullableString = Type.Union([Type.String(), Type.Null()]);
// Record/Map
const Metadata = Type.Record(Type.String(), Type.Unknown());import { Type, type Static } from '@sinclair/typebox';
// Define shared schemas
const ErrorResponse = Type.Object({
error: Type.String(),
message: Type.String(),
statusCode: Type.Integer(),
});
const PaginationQuery = Type.Object({
page: Type.Integer({ minimum: 1, default: 1 }),
limit: Type.Integer({ minimum: 1, maximum: 100, default: 20 }),
});
// Register globally
app.addSchema(Type.Object({ $id: 'ErrorResponse', ...ErrorResponse }));
app.addSchema(Type.Object({ $id: 'PaginationQuery', ...PaginationQuery }));
// Reference in routes
app.get('/items', {
schema: {
querystring: { $ref: 'PaginationQuery#' },
response: {
400: { $ref: 'ErrorResponse#' },
},
},
}, handler);You can also use plain JSON Schema directly:
import Fastify from 'fastify';
const app = Fastify();
const createUserSchema = {
body: {
type: 'object',
properties: {
name: { type: 'string', minLength: 1, maxLength: 100 },
email: { type: 'string', format: 'email' },
age: { type: 'integer', minimum: 0, maximum: 150 },
},
required: ['name', 'email'],
additionalProperties: false,
},
response: {
201: {
type: 'object',
properties: {
id: { type: 'string', format: 'uuid' },
name: { type: 'string' },
email: { type: 'string' },
createdAt: { type: 'string', format: 'date-time' },
},
},
},
};
app.post('/users', { schema: createUserSchema }, async (request, reply) => {
const user = await createUser(request.body);
reply.code(201);
return user;
});Validate different parts of the request:
const fullRequestSchema = {
// URL parameters
params: {
type: 'object',
properties: {
id: { type: 'string', format: 'uuid' },
},
required: ['id'],
},
// Query string
querystring: {
type: 'object',
properties: {
include: { type: 'string', enum: ['posts', 'comments', 'all'] },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
},
},
// Request headers
headers: {
type: 'object',
properties: {
'x-api-key': { type: 'string', minLength: 32 },
},
required: ['x-api-key'],
},
// Request body
body: {
type: 'object',
properties: {
data: { type: 'object' },
},
required: ['data'],
},
};
app.put('/resources/:id', { schema: fullRequestSchema }, handler);Define reusable schemas with $id and reference them with $ref:
// Add shared schemas to Fastify
app.addSchema({
$id: 'user',
type: 'object',
properties: {
id: { type: 'string', format: 'uuid' },
name: { type: 'string' },
email: { type: 'string', format: 'email' },
createdAt: { type: 'string', format: 'date-time' },
},
required: ['id', 'name', 'email'],
});
app.addSchema({
$id: 'userCreate',
type: 'object',
properties: {
name: { type: 'string', minLength: 1 },
email: { type: 'string', format: 'email' },
},
required: ['name', 'email'],
additionalProperties: false,
});
app.addSchema({
$id: 'error',
type: 'object',
properties: {
statusCode: { type: 'integer' },
error: { type: 'string' },
message: { type: 'string' },
},
});
// Reference shared schemas
app.post('/users', {
schema: {
body: { $ref: 'userCreate#' },
response: {
201: { $ref: 'user#' },
400: { $ref: 'error#' },
},
},
}, handler);
app.get('/users/:id', {
schema: {
params: {
type: 'object',
properties: { id: { type: 'string', format: 'uuid' } },
required: ['id'],
},
response: {
200: { $ref: 'user#' },
404: { $ref: 'error#' },
},
},
}, handler);Define schemas for array responses:
app.addSchema({
$id: 'userList',
type: 'object',
properties: {
users: {
type: 'array',
items: { $ref: 'user#' },
},
total: { type: 'integer' },
page: { type: 'integer' },
pageSize: { type: 'integer' },
},
});
app.get('/users', {
schema: {
querystring: {
type: 'object',
properties: {
page: { type: 'integer', minimum: 1, default: 1 },
pageSize: { type: 'integer', minimum: 1, maximum: 100, default: 20 },
},
},
response: {
200: { $ref: 'userList#' },
},
},
}, handler);Add custom validation formats:
import Fastify from 'fastify';
const app = Fastify({
ajv: {
customOptions: {
formats: {
'iso-country': /^[A-Z]{2}$/,
'phone': /^\+?[1-9]\d{1,14}$/,
},
},
},
});
// Or add formats dynamically
app.addSchema({
$id: 'address',
type: 'object',
properties: {
street: { type: 'string' },
country: { type: 'string', format: 'iso-country' },
phone: { type: 'string', format: 'phone' },
},
});Add custom validation keywords:
import Fastify from 'fastify';
import Ajv from 'ajv';
const app = Fastify({
ajv: {
customOptions: {
keywords: [
{
keyword: 'isEven',
type: 'number',
validate: (schema: boolean, data: number) => {
if (schema) {
return data % 2 === 0;
}
return true;
},
errors: false,
},
],
},
},
});
// Use custom keyword
app.post('/numbers', {
schema: {
body: {
type: 'object',
properties: {
value: { type: 'integer', isEven: true },
},
},
},
}, handler);Fastify coerces types by default for query strings and params:
// Query string "?page=5&active=true" becomes:
// { page: 5, active: true } (number and boolean, not strings)
app.get('/items', {
schema: {
querystring: {
type: 'object',
properties: {
page: { type: 'integer' }, // "5" -> 5
active: { type: 'boolean' }, // "true" -> true
tags: {
type: 'array',
items: { type: 'string' }, // "a,b,c" -> ["a", "b", "c"]
},
},
},
},
}, handler);Customize validation error responses:
app.setErrorHandler((error, request, reply) => {
if (error.validation) {
reply.code(400).send({
error: 'Validation Error',
message: 'Request validation failed',
details: error.validation.map((err) => ({
field: err.instancePath || err.params?.missingProperty,
message: err.message,
keyword: err.keyword,
})),
});
return;
}
// Handle other errors
reply.code(error.statusCode || 500).send({
error: error.name,
message: error.message,
});
});Configure the Ajv schema compiler:
import Fastify from 'fastify';
const app = Fastify({
ajv: {
customOptions: {
removeAdditional: 'all', // Remove extra properties
useDefaults: true, // Apply default values
coerceTypes: true, // Coerce types
allErrors: true, // Report all errors, not just first
},
plugins: [
require('ajv-formats'), // Add format validators
],
},
});Handle nullable fields properly:
app.addSchema({
$id: 'profile',
type: 'object',
properties: {
name: { type: 'string' },
bio: { type: ['string', 'null'] }, // Can be string or null
avatar: {
oneOf: [
{ type: 'string', format: 'uri' },
{ type: 'null' },
],
},
},
});Use if/then/else for conditional validation:
app.addSchema({
$id: 'payment',
type: 'object',
properties: {
method: { type: 'string', enum: ['card', 'bank'] },
cardNumber: { type: 'string' },
bankAccount: { type: 'string' },
},
required: ['method'],
if: {
properties: { method: { const: 'card' } },
},
then: {
required: ['cardNumber'],
},
else: {
required: ['bankAccount'],
},
});Organize schemas in a dedicated file:
// schemas/index.ts
export const schemas = [
{
$id: 'user',
type: 'object',
properties: {
id: { type: 'string', format: 'uuid' },
name: { type: 'string' },
email: { type: 'string', format: 'email' },
},
},
{
$id: 'error',
type: 'object',
properties: {
statusCode: { type: 'integer' },
error: { type: 'string' },
message: { type: 'string' },
},
},
];
// app.ts
import { schemas } from './schemas/index.js';
for (const schema of schemas) {
app.addSchema(schema);
}Schemas work directly with @fastify/swagger:
import fastifySwagger from '@fastify/swagger';
import fastifySwaggerUi from '@fastify/swagger-ui';
app.register(fastifySwagger, {
openapi: {
info: {
title: 'My API',
version: '1.0.0',
},
},
});
app.register(fastifySwaggerUi, {
routePrefix: '/docs',
});
// Schemas are automatically converted to OpenAPI definitionsResponse schemas enable fast-json-stringify for serialization:
// With response schema - uses fast-json-stringify (faster)
app.get('/users', {
schema: {
response: {
200: {
type: 'array',
items: { $ref: 'user#' },
},
},
},
}, handler);
// Without response schema - uses JSON.stringify (slower)
app.get('/users-slow', handler);Always define response schemas for production APIs to benefit from optimized serialization.