Skip to main content

Fastify plugins as building blocks for a backend Node.js API

Escrito por:
blog-feature-snyk-container-custom-base-image-recommendations

28 de maio de 2024

0 minutos de leitura

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:

1// Import Fastify using ESM syntax
2import Fastify from "fastify";
3
4const fastify = Fastify({ logger: true });
5
6// Defining a route
7fastify.get("/", async (request, reply) => {
8  return { hello: "world" };
9});
10
11async function startServer() {
12  try {
13    // Start the server
14    const address = await fastify.listen({
15      port: 3000,
16      host: "localhost",
17    });
18
19    // Log the server start information using the address returned by Fastify
20    // Fastify will already log the server start information by default
21    // But here is an example of how you can log the server start information
22    // fastify.log.info(`Server starting up at ${address}`);
23  } catch (err) {
24    fastify.log.error(err);
25    process.exit(1);
26  }
27}
28
29startServer();

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:

1const fastify = require('fastify')({ logger: true });
2
3const helloRoute = async (fastify, options) => {
4  fastify.get('/', async (request, reply) => {
5    return { hello: 'world' };
6  });
7};
8
9fastify.register(helloRoute);
10
11const start = async () => {
12  try {
13    await fastify.listen(3000);
14    fastify.log.info(`server listening on ${fastify.server.address().port}`);
15  } catch (err) {
16    fastify.log.error(err);
17    process.exit(1);
18  }
19};
20start();

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:

1const helloRoute = async (fastify, options) => {
2  fastify.route({
3    method: 'GET',
4    url: '/',
5    schema: {
6      response: {
7        200: {
8          type: 'object',
9          properties: {
10            hello: { type: 'string' },
11          },
12        },
13      },
14    },
15    handler: async (request, reply) => {
16      return { hello: 'world' };
17    },
18  });
19};
20
21fastify.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:

1import pino from "pino";
2
3const logger = pino({
4  transport: {
5    target: "pino-pretty",
6    options: {
7      colorize: true,
8    },
9  },
10});
11
12logger.info("Hello from pino-pretty");
13
14const fastify = Fastify({
15  logger: logger,
16});

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:

1fastify.register(import('@fastify/env'), {
2  dotenv: true,
3  schema: {
4    type: 'object',
5    required: [ 'PORT' ],
6    properties: {
7      PORT: {
8        type: 'string',
9        default: 3000
10      }
11    }
12  }
13})

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:

1try {
2    // wait for all plugins to run 
3    await fastify.ready();
4    // Start the server
5    const address = await fastify.listen({
6      // now we can access the fastify.config.PORT configuration
7      port: fastify.config.PORT,
8      host: "localhost",
9    });

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:

1fastify.register(import('@fastify/swagger'), {
2  routePrefix: '/documentation',
3  swagger: {
4    info: {
5      title: 'Test swagger',
6      description: 'testing the fastify swagger api',
7      version: '0.1.0'
8    },
9    host: 'localhost',
10    schemes: ['http'],
11    consumes: ['application/json'],
12    produces: ['application/json']
13  },
14  exposeRoute: true
15})
16
17fastify.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.

Fastify Plugin @fastify/swagger adds Swagger documentation for backend Node.js APIs.

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.

blog-feature-snyk-container-custom-base-image-recommendations