Organizing Routes Into Modules

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.

4 min read Level 2/5 #fastify#structure#modules
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 →