Overview
Fastify is a web framework for Node.js focused on providing the best developer experience with the least overhead and a powerful plugin architecture. It is one of the fastest web frameworks available, capable of serving up to 76,000 requests per second. Fastify uses JSON Schema for request/response validation and serialization, which not only provides data validation but also accelerates JSON serialization by up to 2-3x compared to JSON.stringify.
Inspired by Hapi and Express, Fastify was created by Matteo Collina and Tomas Della Vedova. It provides first-class TypeScript support, a composable plugin system based on avvio, built-in logging via Pino, and automatic OpenAPI/Swagger documentation generation. The framework is designed for building efficient APIs and microservices while maintaining developer productivity.
Installation
Setup
# Create project
mkdir my-api && cd my-api
npm init -y
# Install Fastify
npm install fastify
# With CLI scaffolding
npm install -g fastify-cli
fastify generate my-api --lang=ts
cd my-api && npm install
# TypeScript setup
npm install typescript @types/node tsx --save-dev
Minimal Server
import Fastify from 'fastify';
const fastify = Fastify({ logger: true });
fastify.get('/', async (request, reply) => {
return { hello: 'world' };
});
fastify.listen({ port: 3000, host: '0.0.0.0' }, (err) => {
if (err) {
fastify.log.error(err);
process.exit(1);
}
});
Routing
Route Methods
// Short-hand
fastify.get('/users', handler);
fastify.post('/users', handler);
fastify.put('/users/:id', handler);
fastify.patch('/users/:id', handler);
fastify.delete('/users/:id', handler);
fastify.head('/users', handler);
fastify.options('/users', handler);
// Full declaration
fastify.route({
method: 'GET',
url: '/users/:id',
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' }
}
},
response: {
200: {
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
email: { type: 'string' }
}
}
}
},
handler: async (request, reply) => {
const { id } = request.params as { id: string };
return getUserById(id);
}
});
// Parametric routes
fastify.get('/users/:id', handler); // /users/123
fastify.get('/files/*', handler); // /files/path/to/file
fastify.get('/example/:id(^\\d+)', handler); // Regex constraint
Route Options
fastify.get('/protected', {
preHandler: [authenticate],
schema: {
headers: {
type: 'object',
required: ['authorization'],
properties: {
authorization: { type: 'string' }
}
}
},
handler: async (request, reply) => {
return { user: request.user };
}
});
Schema Validation
JSON Schema Validation
const createUserSchema = {
body: {
type: 'object',
required: ['name', 'email'],
properties: {
name: { type: 'string', minLength: 1, maxLength: 100 },
email: { type: 'string', format: 'email' },
age: { type: 'integer', minimum: 0, maximum: 150 }
},
additionalProperties: false
},
response: {
201: {
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
email: { type: 'string' },
createdAt: { type: 'string', format: 'date-time' }
}
},
400: {
type: 'object',
properties: {
error: { type: 'string' },
message: { type: 'string' }
}
}
}
};
fastify.post('/users', { schema: createUserSchema }, async (request, reply) => {
const { name, email, age } = request.body as CreateUserBody;
const user = await createUser({ name, email, age });
reply.code(201);
return user;
});
Query String and Params
fastify.get('/search', {
schema: {
querystring: {
type: 'object',
properties: {
q: { type: 'string' },
page: { type: 'integer', default: 1, minimum: 1 },
limit: { type: 'integer', default: 20, minimum: 1, maximum: 100 }
},
required: ['q']
}
}
}, async (request) => {
const { q, page, limit } = request.query as SearchQuery;
return search(q, page, limit);
});
Plugins
Creating Plugins
import fp from 'fastify-plugin';
// Plugin with options
const dbPlugin = fp(async (fastify, opts) => {
const pool = await createPool(opts.connectionString);
fastify.decorate('db', pool);
fastify.addHook('onClose', async () => {
await pool.end();
});
}, {
name: 'database',
fastify: '4.x'
});
// Register plugin
fastify.register(dbPlugin, {
connectionString: process.env.DATABASE_URL
});
// Scoped plugin (encapsulated)
fastify.register(async (instance) => {
instance.decorate('scopedValue', 42);
// scopedValue only available in this scope
});
Common Plugins
| Plugin | Purpose | Install |
|---|
@fastify/cors | CORS headers | npm i @fastify/cors |
@fastify/jwt | JWT authentication | npm i @fastify/jwt |
@fastify/swagger | OpenAPI docs | npm i @fastify/swagger |
@fastify/rate-limit | Rate limiting | npm i @fastify/rate-limit |
@fastify/cookie | Cookie handling | npm i @fastify/cookie |
@fastify/multipart | File uploads | npm i @fastify/multipart |
@fastify/static | Static file serving | npm i @fastify/static |
@fastify/websocket | WebSocket support | npm i @fastify/websocket |
@fastify/helmet | Security headers | npm i @fastify/helmet |
@fastify/postgres | PostgreSQL | npm i @fastify/postgres |
import cors from '@fastify/cors';
import jwt from '@fastify/jwt';
await fastify.register(cors, { origin: true });
await fastify.register(jwt, { secret: process.env.JWT_SECRET });
Hooks (Lifecycle)
| Hook | Description |
|---|
onRequest | First hook, before parsing |
preParsing | Before body parsing |
preValidation | Before schema validation |
preHandler | After validation, before handler |
preSerialization | Before response serialization |
onSend | Before sending response |
onResponse | After response sent |
onError | On error |
// Authentication hook
fastify.addHook('preHandler', async (request, reply) => {
try {
await request.jwtVerify();
} catch (err) {
reply.code(401).send({ error: 'Unauthorized' });
}
});
// Logging hook
fastify.addHook('onResponse', async (request, reply) => {
request.log.info({
url: request.url,
statusCode: reply.statusCode,
responseTime: reply.elapsedTime
});
});
Configuration
Fastify Options
const fastify = Fastify({
logger: {
level: 'info',
transport: {
target: 'pino-pretty',
options: { colorize: true }
}
},
trustProxy: true,
maxParamLength: 200,
bodyLimit: 1048576, // 1MB
caseSensitive: true,
requestTimeout: 30000,
keepAliveTimeout: 72000
});
Environment Configuration
import { envSchema } from 'env-schema';
const config = envSchema({
schema: {
type: 'object',
required: ['PORT', 'DATABASE_URL'],
properties: {
PORT: { type: 'integer', default: 3000 },
DATABASE_URL: { type: 'string' },
NODE_ENV: { type: 'string', default: 'development' },
LOG_LEVEL: { type: 'string', default: 'info' }
}
},
dotenv: true
});
Advanced Usage
Error Handling
// Custom error handler
fastify.setErrorHandler((error, request, reply) => {
request.log.error(error);
if (error.validation) {
reply.code(400).send({
error: 'Validation Error',
message: error.message,
details: error.validation
});
return;
}
reply.code(error.statusCode || 500).send({
error: error.name || 'Internal Server Error',
message: error.message
});
});
// Not found handler
fastify.setNotFoundHandler((request, reply) => {
reply.code(404).send({
error: 'Not Found',
message: `Route ${request.method} ${request.url} not found`
});
});
Swagger/OpenAPI Documentation
import swagger from '@fastify/swagger';
import swaggerUi from '@fastify/swagger-ui';
await fastify.register(swagger, {
openapi: {
info: {
title: 'My API',
version: '1.0.0'
},
servers: [{ url: 'http://localhost:3000' }]
}
});
await fastify.register(swaggerUi, {
routePrefix: '/docs'
});
Testing
import { test } from 'node:test';
import assert from 'node:assert';
import buildApp from '../src/app.js';
test('GET / returns hello', async () => {
const app = buildApp();
const response = await app.inject({
method: 'GET',
url: '/'
});
assert.strictEqual(response.statusCode, 200);
assert.deepStrictEqual(JSON.parse(response.body), { hello: 'world' });
});
Troubleshooting
| Problem | Solution |
|---|
FST_ERR_DEC_ALREADY_PRESENT | Decorator name conflict; use unique names or fastify-plugin |
| Schema validation errors | Check JSON Schema syntax; use ajv-errors for custom messages |
| Plugin not available | Ensure fastify-plugin wraps it to break encapsulation |
| Slow serialization | Define response schemas; Fastify uses fast-json-stringify |
| Memory leaks | Check hook cleanup; use onClose for resource cleanup |
| TypeScript type errors | Use declare module to extend Fastify interfaces |
| CORS issues | Register @fastify/cors before routes |
| 413 Payload Too Large | Increase bodyLimit in Fastify options |