Fastify is a fast and low overhead web framework for Node.js that focuses on developer experience and performance. This comprehensive guide covers everything you need to know about building applications with Fastify.
What is Fastify?
Fastify is a web framework heavily inspired by Hapi and Express but with a focus on performance and developer experience.
const fastify = require('fastify')({ logger: true });
fastify.get('/', async (request, reply) => {
return { hello: 'world' };
});
fastify.listen({ port: 3000 }, (err, address) => {
if (err) throw err;
console.log(`Server listening on ${address}`);
});
Key Features
- High Performance - Up to 3x faster than Express
-
- Schema-based - JSON Schema for validation and serialization
- Extensible - Plugin-based architecture
- TypeScript Support - Built-in TypeScript support
- Logger - Built-in Pino logger
- Hooks - Lifecycle hooks for customization
- Decorators - Extend Fastify instance
Basic Routing
GET Requests
const fastify = require('fastify')();
fastify.get('/users', async (request, reply) => {
return [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
];
});
fastify.get('/users/:id', {
schema: {
params: {
type: 'object',
properties: {
id: { type: 'integer' }
}
}
}
}, async (request, reply) => {
const { id } = request.params;
return { id, name: `User ${id}` };
});
POST Requests
fastify.post('/users', {
schema: {
body: {
type: 'object',
required: ['name', 'email'],
properties: {
name: { type: 'string', minLength: 1 },
email: { type: 'string', format: 'email' }
}
}
}
}, async (request, reply) => {
const { name, email } = request.body;
// Create user...
return { id: 3, name, email }, 201;
});
All HTTP Methods
fastify.get('/route', handler);
fastify.post('/route', handler);
fastify.put('/route', handler);
fastify.patch('/route', handler);
fastify.delete('/route', handler);
fastify.options('/route', handler);
fastify.head('/route', handler);
Route Options
fastify.get('/users', {
schema: { /* validation schemas */ },
preHandler: fastify.auth([authenticate]),
config: { rateLimit: { max: 100 } },
preValidation: async (request, reply) => {
// Pre-validation hook
}
}, async (request, reply) => {
return { users: [] };
});
Schema Validation
Request Validation
// Query string validation
fastify.get('/search', {
schema: {
querystring: {
type: 'object',
properties: {
q: { type: 'string' },
limit: { type: 'integer', default: 10 },
offset: { type: 'integer', default: 0 }
}
}
}
}, async (request) => {
const { q, limit, offset } = request.query;
return { q, limit, offset };
});
// Headers validation
fastify.get('/secure', {
schema: {
headers: {
type: 'object',
required: ['authorization'],
properties: {
authorization: { type: 'string' }
}
}
}
}, handler);
Response Validation
// Response schema for serialization
fastify.get('/user/:id', {
schema: {
params: {
type: 'object',
properties: {
id: { type: 'integer' }
}
},
response: {
200: {
type: 'object',
properties: {
id: { type: 'integer' },
name: { type: 'string' },
email: { type: 'string' }
}
}
}
}
}, async (request) => {
return { id: 1, name: 'John', email: '[email protected]' };
});
Ajv Options
const fastify = require('fastify')({
ajv: {
customOptions: {
removeAdditional: 'all',
useDefaults: true,
coerceTypes: true
}
}
});
Plugins
Creating Plugins
// my-plugin.js
async function myPlugin(fastify, options) {
// Decorate fastify instance
fastify.decorate('utility', {
greet: (name) => `Hello, ${name}!`
});
// Add a route
fastify.get('/plugin-route', async () => 'Plugin route');
// Add hooks
fastify.addHook('onRequest', async (request) => {
console.log('Plugin hook called');
});
}
module.exports = myPlugin;
Registering Plugins
const fastify = require('fastify')();
const myPlugin = require('./my-plugin');
// Register with options
fastify.register(myPlugin, { prefix: '/api' });
// Register multiple
fastify.register(require('./auth-plugin'));
fastify.register(require('./database-plugin'));
Encapsulation
fastify.register(async function (fastify, opts) {
// This route is in child scope
fastify.get('/child-route', async () => 'Child scope');
// Decorations here only affect this scope
fastify.decorate('secret', 'child-secret');
}, { prefix: '/v1' });
Middleware and Hooks
Lifecycle Hooks
// Request hooks
fastify.addHook('onRequest', async (request, reply) => {
// Called for every request
});
fastify.addHook('preParsing', async (request, reply) => {
// Before parsing
});
fastify.addHook('preValidation', async (request, reply) => {
// Before validation
});
fastify.addHook('preHandler', async (request, reply) => {
// Before handler
});
fastify.addHook('handler', async (request, reply) => {
// Around the handler (wrap)
});
fastify.addHook('preSerialization', async (request, reply, payload) => {
// Before serialization
});
fastify.addHook('onSend', async (request, reply, payload) => {
// Modify payload
return payload;
});
fastify.addHook('onResponse', async (request, reply, done) => {
// After response sent
});
fastify.addHook('onTimeout', async (request, reply, done) => {
// On timeout
});
fastify.addHook('onError', async (request, reply, error) => {
// On error
});
Error Handling Hooks
fastify.setErrorHandler((error, request, reply) => {
if (error.validation) {
return reply.status(400).send({
error: 'Validation Error',
message: error.message,
validation: error.validation
});
}
return reply.status(500).send({
error: 'Internal Server Error'
});
});
fastify.setNotFoundHandler((request, reply) => {
reply.status(404).send({
error: 'Not Found',
message: `Route ${request.method} ${request.url} not found`
});
});
Decorators
Adding Decorators
// Decorate instance
fastify.decorate('utils', {
calculate: (a, b) => a + b
});
// Decorate reply
fastify.decorateReply('sendJson', function (data) {
this.send(data);
});
// Decorate request
fastify.decorateRequest('session', null);
fastify.addHook('onRequest', async (request) => {
request.session = { userId: 1 };
});
// Usage
fastify.get('/', async (request, reply) => {
const result = fastify.utils.calculate(1, 2);
reply.sendJson({ result });
});
Request and Reply
Request Object
fastify.get('/request-info', async (request, reply) => {
// Route parameters
request.params;
// Query string
request.query;
// Body (parsed)
request.body;
// Headers
request.headers;
// Method & URL
request.method;
request.url;
// Raw node.js objects
request.raw;
// Request ID
request.id;
// Fastify specific
request.server;
request.log;
return { info: 'received' };
});
Reply Object
fastify.get('/responses', async (request, reply) => {
// Send JSON
return { message: 'hello' };
// Or explicitly
reply.send({ message: 'hello' });
// Status code
reply.code(201);
// Headers
reply.header('Cache-Control', 'no-cache');
reply.headers({ 'X-Custom': 'value' });
// Type
reply.type('application/json');
// Redirect
reply.redirect('/other-route');
// Send with status
reply.code(201).send({ created: true });
// End response
reply.raw.end('Done');
});
Error Handling
Sync and Async Errors
// Sync error (throws)
fastify.get('/error-sync', async () => {
throw new Error('Sync error');
});
// Async error (rejects)
fastify.get('/error-async', async () => {
await somePromiseThatRejects();
});
// Error in promise
fastify.get('/error-promise', async () => {
return Promise.reject(new Error('Async error'));
});
// Use try/catch
fastify.get('/safe', async (request, reply) => {
try {
return await riskyOperation();
} catch (error) {
reply.code(500).send({ error: error.message });
}
});
Custom Error Handling
fastify.setErrorHandler((error, request, reply) => {
request.log.error(error);
if (error.code === 'FST_REQ_VALIDATION') {
return reply.status(400).send({
error: 'Bad Request',
message: error.message,
validation: error.validationContext
});
}
if (error.statusCode === 404) {
return reply.status(404).send({ error: 'Not Found' });
}
return reply.status(500).send({ error: 'Internal Server Error' });
});
Validation Example
const fastify = require('fastify')({ logger: true });
// Users routes
fastify.register(async function usersRoutes(fastify) {
// GET /users
fastify.get('/', {
schema: {
querystring: {
type: 'object',
properties: {
page: { type: 'integer', default: 1 },
limit: { type: 'integer', default: 10 }
}
}
}
}, async (request, reply) => {
const { page, limit } = request.query;
// Fetch users with pagination
return {
users: [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
],
page,
limit
};
});
// POST /users
fastify.post('/', {
schema: {
body: {
type: 'object',
required: ['name', 'email'],
properties: {
name: { type: 'string', minLength: 1, maxLength: 100 },
email: { type: 'string', format: 'email' },
age: { type: 'integer', minimum: 0 }
}
},
response: {
201: {
type: 'object',
properties: {
id: { type: 'integer' },
name: { type: 'string' },
email: { type: 'string' }
}
}
}
}
}, async (request, reply) => {
const { name, email, age } = request.body;
// Create user
const user = {
id: Math.floor(Math.random() * 1000),
name,
email
};
reply.code(201);
return user;
});
// GET /users/:id
fastify.get('/:id', {
schema: {
params: {
type: 'object',
properties: {
id: { type: 'integer' }
}
}
}
}, async (request, reply) => {
const { id } = request.params;
return { id, name: 'John', email: '[email protected]' };
});
}, { prefix: '/users' });
// Start server
fastify.listen({ port: 3000 }, (err) => {
if (err) {
fastify.log.error(err);
process.exit(1);
}
});
Performance Tips
Connection Timeout
const fastify = require('fastify')({
connectionTimeout: 10000,
keepAliveTimeout: 5000
});
Disable Request Logging
const fastify = require('fastify')({
logger: false
});
Schema Caching
const fastify = require('fastify')({
schemaController: {
compilersFactory: {
buildValidatorCompiler() {
return ajvInstance;
}
}
}
});
Testing
const fastify = require('fastify')();
fastify.get('/hello', async (request, reply) => {
return { hello: 'world' };
});
// Using built-in test client
const t = require('tap');
t.test('routes', async (t) => {
t.test('hello route', async (t) => {
const response = await fastify.inject({
method: 'GET',
url: '/hello'
});
t.equal(response.statusCode, 200);
t.same(response.json(), { hello: 'world' });
});
});
// Async/await testing
const fastify = require('./app');
async function test() {
const response = await fastify.inject({
method: 'GET',
url: '/users'
});
console.log(response.statusCode);
console.log(response.body);
await fastify.close();
}
test();
Comparison with Express
| Feature | Fastify | Express |
|---|---|---|
| Performance | 3x faster | Baseline |
| Schema Validation | Built-in | Middleware |
| TypeScript | Native | Community |
| Async/Await | Native | Polyfill |
| Plugin System | Built-in | Connect |
| Logger | Pino | Morgan |
External Resources
Conclusion
Fastify provides excellent performance and developer experience for Node.js applications. Key points:
- Use JSON Schema for validation and serialization
- Leverage the plugin system for modularity
- Take advantage of TypeScript support
- Use hooks for request lifecycle control
- Fastify’s encapsulation prevents plugin conflicts
For high-performance Node.js APIs, Fastify is an excellent choice over Express.
Comments