log
Swift Code Chronicles

Fastify Best Practices #2: autoLoad

Published on June 21, 2025
Updated on June 26, 2025
37 min read
TypeScript

In the development of any web framework, as a project’s scale increases, how to organize code—especially routes—efficiently and clearly becomes a core problem. A good automated configuration process can significantly improve our development experience and the project’s maintainability.

The Pain Point: The Hassle of Manual Registration

In Fastify, adding an API route is very straightforward:

// index.ts
import fastify from 'fastify';

const server = fastify();

// Add a new API
server.get('/ping', async (request, reply) => {
  return 'pong\n';
});

// Start the server...

This looks simple when the project consists of only one file. But as the number of APIs grows, we naturally want to split the logic for each route into separate files to keep the code clean. The code structure then evolves into this:

// routes/ping.ts
import { FastifyInstance, FastifyPluginOptions } from 'fastify';

// It's good practice to wrap routes in Fastify plugins
export default async (fastify: FastifyInstance, opts: FastifyPluginOptions) => {
  fastify.get('/ping', async (request, reply) => {
    return 'pong\n';
  });
};
// index.ts
import pingRoute from './routes/ping';
import fastify from 'fastify';

const server = fastify();

// Every time a route file is added, it needs to be manually registered here
server.register(pingRoute);

// server.register(userRoute);
// server.register(productRoute);
// ... more routes

At this point, you’ll notice two obvious problems:

  1. Burden of Manual Maintenance: Every time a new route file is created, you must remember to manually import and register it in the main entry file (index.ts). This is not only tedious but also prone to omissions.
  2. Implicit Repetition: The path of the route file (e.g., routes/ping.ts) and the API endpoint it defines (/ping) are semantically repetitive.

Can we make the file structure directly map to the API paths? To solve these problems, the official Fastify ecosystem provides a powerful tool: @fastify/autoload.

The Solution: Automation with @fastify/autoload

@fastify/autoload allows us to specify a directory, and it will automatically scan all files within that directory, registering them as plugins (including routes) to the Fastify instance.

1. Auto-loading Routes

Suppose we need to define the following APIs:

POST /management/user
DELETE /management/user/{id}
GET /user
GET /user/{id}

We can create a corresponding file and directory structure to let autoload generate these routes automatically. By convention, path parameters like {id} should be represented by a directory or file name prefixed with an underscore, such as _id.

The directory structure can be designed as follows:

src
 ┣ routes
 ┃ ┣ management
 ┃ ┃ ┗ user
 ┃ ┃ ┃ ┣ _id
 ┃ ┃ ┃ ┃ ┗ delete.ts  // -> DELETE /management/user/:id
 ┃ ┃ ┃ ┗ post.ts      // -> POST /management/user
 ┃ ┗ user
 ┃ ┃ ┣ _id
 ┃ ┃ ┃ ┗ get.ts       // -> GET /user/:id
 ┃ ┃ ┗ get.ts         // -> GET /user
 ┣ plugins
 ┃ ┗ ...
 ┗ index.ts

Next, we write the route files. Since autoload generates the route prefix based on the file path, we only need to define the path relative to that prefix in the route file. Typically, for index.ts or files named after HTTP methods (like get.ts, post.ts), the path can be an empty string ''.

// src/routes/management/user/post.ts
import { FastifyInstance } from 'fastify';

// Use a default export, and the function must be async
export default async function (fastify: FastifyInstance) {
  // The path here is an empty string; autoload will automatically add the /management/user prefix
  fastify.post('', async (_request, reply) => {
    // Actual business logic
    return reply.status(201).send({ message: 'User created' });
  });
}

Finally, configure autoload in the main entry file index.ts:

// src/index.ts
import autoLoad from '@fastify/autoload';
import fastify from 'fastify';
import { join } from 'path';

const server = fastify({ logger: true });

// Register route auto-loading
server.register(autoLoad, {
  dir: join(__dirname, 'routes'), // Specify the directory where routes are located
  routeParams: true,
  dirNameRoutePrefix: true,
});

server.listen({ port: 3000 });

Analysis of Core autoLoad Options

  • dir: A mandatory option that points to the root directory of your routes or plugins.
  • routeParams: true: This is key to implementing dynamic routes. When enabled, autoload will convert file or directory names prefixed with an underscore _ (e.g., _id) into Fastify path parameters (e.g., /:id).
  • dirNameRoutePrefix: true: Tells autoload to use the directory structure to generate the route prefix. If this is disabled, routes/management/user/post.ts will only be registered as /post instead of /management/user/post.

2. Auto-loading Plugins

Besides routes, autoload can also load custom plugins. Plugins are an excellent way to encapsulate reusable logic (like database connections, authentication, decorators, etc.). To better manage dependencies and encapsulation between plugins, we typically use fastify-plugin.

First, install the dependency:

npm i fastify-plugin

Now, let’s create a plugin that “decorates” the Fastify instance with a configuration object.

// src/plugins/config.plugin.ts
import { FastifyPluginAsync } from 'fastify';
import fp from 'fastify-plugin';

// Wrapping the plugin with fp() prevents Fastify's encapsulation from isolating the plugin.
// This means that what is decorated in this plugin will be visible to all outer child contexts.
const configPlugin: FastifyPluginAsync = async (fastify, opts) => {
  const config = {
    dbUrl: process.env.DB_URL || 'default-db-url',
    apiKey: process.env.API_KEY,
  };

  // Use decorate to attach the config object to the Fastify instance
  fastify.decorate('config', config);
};

export default fp(configPlugin);

We can place all our plugins in a dedicated plugins directory. Then, use autoload again to load them.

// Continue adding in index.ts
// ...
// Register plugin auto-loading
server.register(autoLoad, {
  dir: join(__dirname, 'plugins'),
  // Using matchFilter allows for finer control over which files are loaded
  matchFilter: (path) => path.includes('plugin'),
});

// Register routes (Note: plugins should be registered before routes)
server.register(autoLoad, {
  dir: join(__dirname, 'routes'),
  // ...
});

The matchFilter option is very useful; it accepts a file path and returns a boolean, allowing you to decide whether to load a file based on its name, extension, or other rules.

Summary

@fastify/autoload is a seemingly simple yet extremely powerful tool. By embracing the “convention over configuration” philosophy, it helps us:

  • Eliminate Boilerplate Code: No more need to manually import and register every route and plugin.
  • Optimize Project Structure: Intuitively reflect the API structure through the file system’s directory structure.
  • Improve Development Efficiency: Developers can focus on implementing business logic, leaving the rest to automation tools.

In a continuously growing project, introducing autoLoad early on is a wise investment. It enables your Fastify application to maintain a high degree of organization and maintainability right from the start.

About

A personal blog sharing technical insights, experiences and thoughts

Quick Links

Contact

  • Email: hushukang_blog@proton.me
  • GitHub

© 2025 Swift Code Chronicles. All rights reserved