One Plugin Per Feature
Organizing Routes Into Modules
Group related routes into a plugin file, then compose them in the app boundary — each module stays testable and easy to relocate.
What you'll learn
- Split routes into files like routes/users.ts
- Export the plugin as a default async function
- Compose modules with app.register
The plugin pattern scales naturally: one file per feature, one register call to wire it in. Tests load a single plugin into a throwaway app — no need to boot the whole service.
A Feature Plugin
// src/routes/users.ts
import type { FastifyPluginAsync } from 'fastify';
const usersRoutes: FastifyPluginAsync = async (app) => {
app.get('/', async () => app.db.users.findMany());
app.get('/:id', {
schema: {
params: {
type: 'object',
properties: { id: { type: 'integer' } },
},
},
handler: async (req) => app.db.users.find(req.params.id),
});
app.post('/', async (req) => app.db.users.create(req.body));
};
export default usersRoutes; Mounting at the Boundary
// src/app.ts
import Fastify from 'fastify';
import dbPlugin from './plugins/db.js';
import authPlugin from './plugins/auth.js';
import usersRoutes from './routes/users.js';
import postsRoutes from './routes/posts.js';
export async function buildApp() {
const app = Fastify({ logger: true });
await app.register(dbPlugin);
await app.register(authPlugin);
await app.register(usersRoutes, { prefix: '/api/users' });
await app.register(postsRoutes, { prefix: '/api/posts' });
return app;
} buildApp returns a fresh app for tests and for index.ts to listen on.
Auto-Loading
For larger apps, @fastify/autoload walks a directory and registers every plugin it finds:
import autoload from '@fastify/autoload';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const here = dirname(fileURLToPath(import.meta.url));
await app.register(autoload, { dir: join(here, 'plugins') });
await app.register(autoload, { dir: join(here, 'routes'), options: { prefix: '/api' } }); Convention over configuration without the magic — plugins live in clear folders.
onSend Hook — Mutate the Outgoing Payload →