In the world of building backend Node.js APIs, Fastify stands out with its plugin ecosystem and architecture approach, offering a compelling option beyond the conventional Express framework. This highly efficient, low-overhead web framework distinguishes itself through its remarkable speed and streamlined simplicity. At the heart of Fastify's design is a robust plugin architecture that leverages the asynchronous capabilities of Node.js to the fullest, setting a new standard in performance among Node.js frameworks by emphasizing the integral role of Fastify Plugins.
Fastify's development is driven by a growing open source community, ensuring it stays relevant and up-to-date with the latest trends in web application development. If you practice backend web development in 2024, Fastify has proven to be a valuable framework for building fast, scalable, and secure web applications.
Why Fastify and its plugins make a robust backend Node.js API
When it comes to developing modern web applications, Fastify has emerged as a compelling choice among developers. This web framework, built for Node.js, offers a compelling blend of performance, flexibility, and developer-friendly features. This section will explore the benefits of using Fastify for modern application development.
Modern ESM support
Fastify has direct support for ECMAScript modules (ESM). This is important because ESM is the official format for package JavaScript modules. With Fastify, you can natively use the import and export syntax without any transpilation step.
Here is a simple example of a Fastify server using ESM:
import fastify from 'fastify';
const server = fastify();
server.get('/', async (request, reply) => {
return { hello: 'world' }
});
server.listen({port: 3000}, (err, address) => {
if (err) throw err
server.log.info(`Server listening at ${address}`)
});
Proper async/await promise-based route definitions
Fastify facilitates easier route handling using async/await syntax. This results in cleaner, more readable code as compared to traditional callback patterns.
Express doesn't natively support async/await in route definitions, which often leads to callback hell or the need to wrap route handlers in try/catch
blocks to handle errors. Fastify's native support for async/await makes it easier to write and maintain asynchronous code using promises.
Here is an example showing how Fastify uses async/await in route definition:
server.get('/api/tasks', async (request, reply) => {
const tasks = await Tasks.findAll();
return tasks;
});
High performance with fast radix route tree resolution
Fastify is designed with performance in mind. It features a fast radix route tree resolution, which is a highly efficient routing algorithm. This allows Fastify to handle many routes with minimal overhead, resulting in fast response times even under heavy load.
A rich Fastify plugin architecture with encapsulation
Fastify has a rich plugin architecture that supports encapsulation. This means you can easily break down your application into isolated components, each with its own set of routes, plugins, and decorators. This promotes better code organization and reduces the risk of name collisions.
Here's an example of a Fastify plugin that decorates the route handler with a utility function:
import fp from 'fastify-plugin';
export default fp(async function (fastify, opts) {
fastify.decorate('utility', () => 'This is a utility function');
});
You'd use this plugin as follows:
// import and get a fastify server instance,
// and then:
server.register(import('./utility-plugin.js'));
server.get('/', async (request, reply) => {
return server.utility();
});
Lightweight dependency injection
Fastify's plugin system also acts as a lightweight dependency injection (DI) system. This allows for easy sharing of common utilities and services across your application without resorting to singletons or global variables.
In the following example, a database connection is shared across different parts of the application:
fastify.register(async function (fastify, opts) {
const db = await someDbConnectionLibrary(opts.connectionString);
fastify.decorate('db', db);
});
A simple backend Node.js API with Fastify
This blog post will focus on the foundational building blocks of building backend Node.js APIs using Fastify and its recommended plugins in 2024. We will dive deep into the essential components of a Fastify application, from routing and middleware to validation and serialization.
Furthermore, given the importance of cybersecurity in today's digital world, we will briefly explore aspects related to web security and how to use Fastify plugins to handle them, a crucial aspect often overlooked in the haste of application development.
To begin, ensure that you have the latest Node.js LTS version installed on your system (at the date of writing, it is v20.11.1). Then, initialize a new Node.js project and install Fastify using npm, as shown below:
$ mkdir fastify-app && cd fastify-app
$ npm init -y
$ npm install --save fastify
Edit the package.json
file to add a top level type: module
key definition to enable ESM support:
{
"type": "module",
}
We can then continue with the basic structure of a Fastify application in a server.js
file in the root directory of the project:
// Import Fastify using ESM syntax
import Fastify from "fastify";
const fastify = Fastify({ logger: true });
// Defining a route
fastify.get("/", async (request, reply) => {
return { hello: "world" };
});
async function startServer() {
try {
// Start the server
const address = await fastify.listen({
port: 3000,
host: "localhost",
});
// Log the server start information using the address returned by Fastify
// Fastify will already log the server start information by default
// But here is an example of how you can log the server start information
// fastify.log.info(`Server starting up at ${address}`);
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
}
startServer();
Encapsulating the API route in a Fastify plugin
Fastify promotes the use of plugins to organize your code and enable encapsulation. This allows you to keep related code bundled together and helps avoid conflicts in different parts of your application.
Let's refactor our "Hello World" route into a Fastify plugin:
const fastify = require('fastify')({ logger: true });
const helloRoute = async (fastify, options) => {
fastify.get('/', async (request, reply) => {
return { hello: 'world' };
});
};
fastify.register(helloRoute);
const start = async () => {
try {
await fastify.listen(3000);
fastify.log.info(`server listening on ${fastify.server.address().port}`);
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
};
start();
Using route request and response schema
Fastify provides a way to validate incoming requests and structure outgoing responses using JSON schema. This helps to ensure the data integrity of your application and simplifies the process of creating documentation.
Here's how you can define a request and response schema for the "Hello World" route:
const helloRoute = async (fastify, options) => {
fastify.route({
method: 'GET',
url: '/',
schema: {
response: {
200: {
type: 'object',
properties: {
hello: { type: 'string' },
},
},
},
},
handler: async (request, reply) => {
return { hello: 'world' };
},
});
};
fastify.register(helloRoute);
Now, Fastify will automatically validate the response of the "Hello World" route against the provided schema. This helps catch potential bugs and ensures your application behaves as expected.
Fastify plugins as building blocks of Fastify applications
In this section, we will take a look at four key Fastify plugins: @fastify/pino
, @fastify/cors
, @fastify/env
, and @fastify/swagger
along with @fastify/swagger-ui
. These plugins are helpful in forming the backbone of any Fastify application when building a backend Node.js API.
The Pino logger for Fastify
Pino is an exceptionally high-performance logger for Node.js, with its own plugins that extend it to integrate with other logging systems such as Sentry and others.
You can take advantage of the fact that Fastify uses Pino as its default logger. You're already utilizing Pino under the hood when you instantiate Fastify with { logger: true }
. However, you can pass a configuration object to the logger property if you want more control over the logging level or other Pino-specific configurations.
During development, you might want a friendlier and more colorful log output, so we can use the pino-pretty
package to achieve this. First, install both pino
and pino-pretty
:
npm install --save pino pino-pretty
And configure pino-pretty
as follows:
import pino from "pino";
const logger = pino({
transport: {
target: "pino-pretty",
options: {
colorize: true,
},
},
});
logger.info("Hello from pino-pretty");
const fastify = Fastify({
logger: logger,
});
In the above, we instantiate a logger using pino and configure it to use pino-pretty
as the transport target, providing it with the colorize
option to enable colorful log output. We then pass our custom logger
variable to Fastify's logger
option.
This transforms the log output into a more human-readable format, making it easier to read and understand:
[12:13:43.499] INFO (95477): hello from pino-pretty
[12:13:43.517] INFO (95477): Server listening at http://[::1]:3000
[12:13:43.518] INFO (95477): Server listening at http://127.0.0.1:3000
The benefits of using Pino in Fastify include:
- Faster logging: Pino's primary goal is to be the quickest logger for Node.js.
- Versatile logger: It supports different log levels and can format logs as JSON.
The Fastify plugin @fastify/cors
The@fastify/cors
plugin provides a simple way to use CORS (cross-origin resource sharing) in your Fastify application.
Benefits:
- Secure: Helps to manage the flow of data between different origins.
- Customizable: Allows you to define a custom function to set CORS options.
Here's a simple example showing how to add and use the @fastify/cors
plugin:
// after creating the fastify instance
// let's register the fastify cors plugin:
fastify.register(require('@fastify/cors'), {
origin: '*',
methods: ['GET','POST', 'PUT', 'DELETE']
})
We can then test whether the CORS configuration is working by making a cURL request to the Fastify server:
curl -i http://localhost:3000
The response should include the Access-Control-Allow-Origin
header, indicating that the CORS configuration is working as expected:
HTTP/1.1 200 OK
access-control-allow-origin: *
content-type: application/json; charset=utf-8
content-length: 17
Date: Sun, 18 Feb 2024 10:20:15 GMT
Connection: keep-alive
Keep-Alive: timeout=72
The Fastify plugin @fastify/env
It's common for Node.js developers to use environment variables to configure their applications and even more so to utilize the .env
file to manage these variables.
This is where the @fastify/env
Fastify plugin comes in handy. While you can use dotenv
or Node.js built-in --env-file
to load configuration, this Fastify plugin allows you to interact with the configuration of these environment variables directly from your Fastify application while also providing configuration validation and type conversion.
Here's a simple example showing how to add and use the @fastify/env
plugin. Start by installing the plugin and creating a simple .env
file:
npm install --save @fastify/env
echo "PORT=3001" > .env
And then update the server.js
code to register this new Fastify plugin:
fastify.register(import('@fastify/env'), {
dotenv: true,
schema: {
type: 'object',
required: [ 'PORT' ],
properties: {
PORT: {
type: 'string',
default: 3000
}
}
}
})
Registering the @fastify/env
Fastify plugin provides access to the configuration defined in the .env
file via the fastify.config
object. However, we can't just use it directly. We need to introduce a new concept related to Fastify plugin architecture.
With Fastify's asynchronous plugin architecture, it is required to wait for Fastify to initialize all the plugins before they are actually applied. Doing so ensures that the plugins are properly registered and ready to be used, and if you had skipped this step, you would have encountered an error when trying to access the fastify.config
object.
So our new startServer() function needs to be updated as follows:
try {
// wait for all plugins to run
await fastify.ready();
// Start the server
const address = await fastify.listen({
// now we can access the fastify.config.PORT configuration
port: fastify.config.PORT,
host: "localhost",
});
Benefits of using the @fastify/env
plugin:
- Environment variable validation: Ensures required environment variables are present.
- Type conversion: Converts environment variables to the required types.
- Light-weight dependency injection: Allows for easy sharing of configuration across your application wherever you can access the
fastify
application instance.
Document backend Node.js APIs with @fastify/swagger and @fastify/swagger-ui Fastify plugins
The @fastify/swagger
plugin helps generate and serve a swagger documentation page for your Fastify application. @fastify/swagger-ui
is a plugin that provides a built-in UI for your swagger documentation that can be accessed via a web browser and the Fastify application itself serves this Swagger UI web page.
As before, we'll begin by adding these plugins to our Fastify application:
npm install --save @fastify/swagger @fastify/swagger-ui
Then, we continue to register these two plugins in our server.js
file:
fastify.register(import('@fastify/swagger'), {
routePrefix: '/documentation',
swagger: {
info: {
title: 'Test swagger',
description: 'testing the fastify swagger api',
version: '0.1.0'
},
host: 'localhost',
schemes: ['http'],
consumes: ['application/json'],
produces: ['application/json']
},
exposeRoute: true
})
fastify.register(import('@fastify/swagger-ui'))
Restart the Fastify server and open this URL http://localhost:3001/documentation
in your web browser to see the generated Swagger UI documentation page. And just like that, you have a fully functional Swagger documentation page for your Fastify application.
A note regarding security concerns — in a production environment, you should consider securing the Swagger UI documentation page with authentication and authorization and avoid using your production Node.js servers to serve the Swagger UI documentation page.
Benefits:
- Easy API documentation: Automatically generates API documentation.
- Customizable: Allows customization of the documentation page.
Bringing it all together
As we've explored, Fastify's vibrant ecosystem offers a variety of plugins that can significantly enhance your backend Node.js API development process. From improving logging capabilities with pino
and pino-pretty
to ensuring CORS browser security through @fastify/cors
, managing environment variables with @fastify/env
, and documenting your API with @fastify/swagger
and `@fastify/swagger-ui, these tools are designed to streamline your workflow, enhance performance, and elevate the quality of your applications.
Our learnings about Fastify plugins can be summarized as follows:
Embracing modular development: Fastify's plugin architecture promotes modular development and encourages a clean and maintainable codebase. By integrating these plugins, developers can focus on writing business logic rather than boilerplate code, enabling rapid development without compromising performance or scalability.
Boosting productivity and performance: Each plugin, with its specific focus, addresses common web development challenges, allowing you to build robust, efficient, and secure APIs. The simplicity of their integration with Fastify, coupled with the benefits they bring, such as enhanced logging, cross-origin resource sharing, environment configuration, and API documentation, directly contributes to increased developer productivity and application performance.
Encouraging further exploration: While we've highlighted a few essential plugins, the Fastify ecosystem is rich with many other plugins addressing various needs, from authentication to database integration, rate limiting, and more. We encourage developers to explore the Fastify plugins page and experiment with these tools to discover how they can further optimize and enhance their API development process.
You are welcome to follow the complete source code for all of the above examples of a backend Node.js API with the Fastify plugins we discussed.