Fastify Best Practices #2: autoLoad
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:
- Burden of Manual Maintenance: Every time a new route file is created, you must remember to manually
importandregisterit in the main entry file (index.ts). This is not only tedious but also prone to omissions. - 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,autoloadwill convert file or directory names prefixed with an underscore_(e.g.,_id) into Fastify path parameters (e.g.,/:id).dirNameRoutePrefix: true: Tellsautoloadto use the directory structure to generate the route prefix. If this is disabled,routes/management/user/post.tswill only be registered as/postinstead 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
importandregisterevery 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.