Fastify plugins as building blocks for a backend Node.js API
Tags: applicationsecurity, javascript, node
Published: 2024-05-29 02:00:41

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.


![](https://res.cloudinary.com/snyk/image/upload/v1716912131/blog-fastify-plugin.jpg)
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](https://github.com/snyk-snippets/fastify-plugins-for-backend-nodejs-api-development.) for all of the above examples of a backend Node.js API with the Fastify plugins we discussed.