Fastify best practices skill
93
97%
Does it follow best practices?
Impact
85%
1.37xAverage score across 4 eval scenarios
Passed
No known issues
Define routes with the shorthand methods or the full route method:
import Fastify from 'fastify';
const app = Fastify();
// Shorthand methods
app.get('/users', async (request, reply) => {
return { users: [] };
});
app.post('/users', async (request, reply) => {
return { created: true };
});
// Full route method with all options
app.route({
method: 'GET',
url: '/users/:id',
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' },
},
required: ['id'],
},
},
handler: async (request, reply) => {
return { id: request.params.id };
},
});Access URL parameters through request.params:
// Single parameter
app.get('/users/:id', async (request) => {
const { id } = request.params as { id: string };
return { userId: id };
});
// Multiple parameters
app.get('/users/:userId/posts/:postId', async (request) => {
const { userId, postId } = request.params as { userId: string; postId: string };
return { userId, postId };
});
// Wildcard parameter (captures everything after)
app.get('/files/*', async (request) => {
const path = (request.params as { '*': string })['*'];
return { filePath: path };
});
// Regex parameters (Fastify uses find-my-way)
app.get('/orders/:id(\\d+)', async (request) => {
// Only matches numeric IDs
const { id } = request.params as { id: string };
return { orderId: parseInt(id, 10) };
});Access query parameters through request.query:
app.get('/search', {
schema: {
querystring: {
type: 'object',
properties: {
q: { type: 'string' },
page: { type: 'integer', default: 1 },
limit: { type: 'integer', default: 10, maximum: 100 },
},
required: ['q'],
},
},
handler: async (request) => {
const { q, page, limit } = request.query as {
q: string;
page: number;
limit: number;
};
return { query: q, page, limit };
},
});Access the request body through request.body:
app.post('/users', {
schema: {
body: {
type: 'object',
properties: {
name: { type: 'string', minLength: 1 },
email: { type: 'string', format: 'email' },
age: { type: 'integer', minimum: 0 },
},
required: ['name', 'email'],
},
},
handler: async (request, reply) => {
const user = request.body as { name: string; email: string; age?: number };
// Create user...
reply.code(201);
return { user };
},
});Access request headers through request.headers:
app.get('/protected', {
schema: {
headers: {
type: 'object',
properties: {
authorization: { type: 'string' },
},
required: ['authorization'],
},
},
handler: async (request) => {
const token = request.headers.authorization;
return { authenticated: true };
},
});Use reply methods to control the response:
app.get('/examples', async (request, reply) => {
// Set status code
reply.code(201);
// Set headers
reply.header('X-Custom-Header', 'value');
reply.headers({ 'X-Another': 'value', 'X-Third': 'value' });
// Set content type
reply.type('application/json');
// Redirect
// reply.redirect('/other-url');
// reply.redirect(301, '/permanent-redirect');
// Return response (automatic serialization)
return { status: 'ok' };
});
// Explicit send (useful in non-async handlers)
app.get('/explicit', (request, reply) => {
reply.send({ status: 'ok' });
});
// Stream response
app.get('/stream', async (request, reply) => {
const stream = fs.createReadStream('./large-file.txt');
reply.type('text/plain');
return reply.send(stream);
});Organize routes by feature/domain in separate files:
src/
routes/
users/
index.ts # Route definitions
handlers.ts # Handler functions
schemas.ts # JSON schemas
posts/
index.ts
handlers.ts
schemas.ts// routes/users/schemas.ts
export const userSchema = {
type: 'object',
properties: {
id: { type: 'string', format: 'uuid' },
name: { type: 'string' },
email: { type: 'string', format: 'email' },
},
};
export const createUserSchema = {
body: {
type: 'object',
properties: {
name: { type: 'string', minLength: 1 },
email: { type: 'string', format: 'email' },
},
required: ['name', 'email'],
},
response: {
201: userSchema,
},
};
// routes/users/handlers.ts
import type { FastifyRequest, FastifyReply } from 'fastify';
export async function createUser(
request: FastifyRequest<{ Body: { name: string; email: string } }>,
reply: FastifyReply,
) {
const { name, email } = request.body;
const user = await request.server.db.users.create({ name, email });
reply.code(201);
return user;
}
export async function getUsers(request: FastifyRequest) {
return request.server.db.users.findAll();
}
// routes/users/index.ts
import type { FastifyInstance } from 'fastify';
import { createUser, getUsers } from './handlers.js';
import { createUserSchema } from './schemas.js';
export default async function userRoutes(fastify: FastifyInstance) {
fastify.get('/', getUsers);
fastify.post('/', { schema: createUserSchema }, createUser);
}Add constraints to routes for versioning or host-based routing:
// Version constraint
app.get('/users', {
constraints: { version: '1.0.0' },
handler: async () => ({ version: '1.0.0', users: [] }),
});
app.get('/users', {
constraints: { version: '2.0.0' },
handler: async () => ({ version: '2.0.0', data: { users: [] } }),
});
// Client sends: Accept-Version: 1.0.0
// Host constraint
app.get('/', {
constraints: { host: 'api.example.com' },
handler: async () => ({ api: true }),
});
app.get('/', {
constraints: { host: 'www.example.com' },
handler: async () => ({ web: true }),
});Use prefixes to namespace routes:
// Using register
app.register(async function (fastify) {
fastify.get('/list', async () => ({ users: [] }));
fastify.get('/:id', async (request) => ({ id: request.params.id }));
}, { prefix: '/users' });
// Results in:
// GET /users/list
// GET /users/:idHandle multiple HTTP methods with one handler:
app.route({
method: ['GET', 'HEAD'],
url: '/resource',
handler: async (request) => {
return { data: 'resource' };
},
});Customize the not found handler:
app.setNotFoundHandler({
preValidation: async (request, reply) => {
// Optional pre-validation hook
},
preHandler: async (request, reply) => {
// Optional pre-handler hook
},
}, async (request, reply) => {
reply.code(404);
return {
error: 'Not Found',
message: `Route ${request.method} ${request.url} not found`,
statusCode: 404,
};
});Handle method not allowed responses:
// Fastify doesn't have built-in 405 handling
// Implement with a custom not found handler that checks allowed methods
app.setNotFoundHandler(async (request, reply) => {
// Check if the URL exists with a different method
const route = app.hasRoute({
url: request.url,
method: 'GET', // Check other methods
});
if (route) {
reply.code(405);
return { error: 'Method Not Allowed' };
}
reply.code(404);
return { error: 'Not Found' };
});Apply configuration to specific routes:
app.get('/slow-operation', {
config: {
rateLimit: { max: 10, timeWindow: '1 minute' },
},
handler: async (request) => {
return { result: await slowOperation() };
},
});
// Access config in hooks
app.addHook('onRequest', async (request, reply) => {
const config = request.routeOptions.config;
if (config.rateLimit) {
// Apply rate limiting
}
});Register routes from async sources:
app.register(async function (fastify) {
const routeConfigs = await loadRoutesFromDatabase();
for (const config of routeConfigs) {
fastify.route({
method: config.method,
url: config.path,
handler: createDynamicHandler(config),
});
}
});Use @fastify/autoload to automatically load routes from a directory structure:
import Fastify from 'fastify';
import autoload from '@fastify/autoload';
import { join } from 'node:path';
const app = Fastify({ logger: true });
// Auto-load plugins
app.register(autoload, {
dir: join(import.meta.dirname, 'plugins'),
options: { prefix: '' },
});
// Auto-load routes
app.register(autoload, {
dir: join(import.meta.dirname, 'routes'),
options: { prefix: '/api' },
});
await app.listen({ port: 3000 });Directory structure:
src/
plugins/
database.ts # Loaded automatically
auth.ts # Loaded automatically
routes/
users/
index.ts # GET/POST /api/users
_id/
index.ts # GET/PUT/DELETE /api/users/:id
posts/
index.ts # GET/POST /api/postsRoute file example:
// routes/users/index.ts
import type { FastifyPluginAsync } from 'fastify';
const users: FastifyPluginAsync = async (fastify) => {
fastify.get('/', async () => {
return fastify.repositories.users.findAll();
});
fastify.post('/', async (request) => {
return fastify.repositories.users.create(request.body);
});
};
export default users;