Advanced Fastify: Hooks, Middleware, and Decorators
Tags: node, fastify
Published: 2023-05-31 12:00:03

In the first article of this series, we introduced Fastify and compared it to Express, highlighting the benefits of switching to Fastify for high-performance web applications. We also explored Fastify's plugin system in detail, showing you how to extend and customize your web applications with reusable modules.

In this part, we'll dive deeper into some of Fastify's more advanced concepts. Specifically, we'll demonstrate how to use hooks, middleware, decorators, and validation to build more robust web applications. We'll also briefly touch on some viable strategies for migrating an existing Express application to Fastify.

Let's get started!

## Hooks and Middleware in Fastify

Hooks in Fastify allow you to listen for specific events in your application's lifecycle and perform specific tasks when those events occur. Hooks are analogous to Express-style middleware functions but are more performant, allowing you to perform tasks such as authentication, logging, routing, data parsing, error handling, and more. There are two types of hooks in Fastify: [request/reply hooks](https://www.fastify.io/docs/latest/Reference/Hooks/#requestreply-hooks) and [application hooks](https://www.fastify.io/docs/latest/Reference/Hooks/#application-hooks).

### Request/Reply Hooks in Fastify

Request/reply hooks are executed during a server's request/reply cycle, allowing you to perform various tasks before or after an incoming request or outgoing response is processed. These hooks can be applied to all routes or a selection of routes. They are typically used to implement features such as:

- Access control
- Data validation
- Request logging

And more.

Examples of request/reply hooks include `preRequest`, `onSend`, `onTimeout`, and others.

Here's an example that uses the `onSend` hook to add a `Server` header to all responses sent from a server:

```javascript
fastify.addHook("onSend", (request, reply, payload, done) => {
  reply.headers({
    Server: "fastify",
  });

  done();
});
```

The `request` and `reply` objects should be familiar to you by now, and the `payload` parameter represents the response body. You can modify the response payload here or clear it altogether by setting it to `null` in the hook function. Finally, the `done()` callback should be executed to signify the end of the hook so that the request/reply lifecycle continues. It can take up to two arguments: an error (if any), or `null`, and the updated payload.

With the above code in place, each response will now contain the specified headers:

```shell
curl -I http://localhost:3000
```

```text
HTTP/1.1 200 OK
content-type: text/plain; charset=utf-8
server: fastify
content-length: 12
Date: Sun, 12 Feb 2023 18:23:40 GMT
Connection: keep-alive
Keep-Alive: timeout=72
```

If you want to implement hooks for a subset of routes, you need to create a new encapsulation context and then register the hook on the `fastify` instance in the plugin. For example, you can create another `onSend` hook in the `health` plugin we demonstrated earlier in this tutorial:

```javascript
// plugin.js
function health(fastify, options, next) {
  fastify.get("/health", (request, reply) => {
    reply.send({ status: "up" });
  });

  fastify.addHook("onSend", (request, reply, payload, done) => {
    const newPlayload = payload.replace("up", "down");
    reply.headers({
      "Cache-Control": "no-store",
      Server: "nodejs",
    });

    done(null, newPlayload);
  });

  next();
}

export default health;
```

This time, the `onSend` hook is used to modify the response body for all the routes in the plugin context (just `/health`, in this case) by changing `up` to `down`, and it also overrides the `Server` response header from the parent context while adding a new `Cache-Control` header. Hence, requests to the `/health` route will now produce the following response:

```shell
curl -i http://localhost:3000/health
```

```text
HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
cache-control: no-store
server: nodejs
content-length: 17
Date: Sun, 12 Feb 2023 18:54:21 GMT
Connection: keep-alive
Keep-Alive: timeout=72

{"status":"down"}⏎
```

Note that the `onSend` hook in the `health` context will run after all shared `onSend` hooks. If you want to register a hook on a specific route instead of all the routes in the plugin context, you can add it to the route options as shown below:

```javascript
function health(fastify, options, next) {
  fastify.get(
    "/health",
    {
      onSend: function (request, reply, payload, done) {
        const newPlayload = payload.replace("up", "down");
        reply.headers({
          "Cache-Control": "no-store",
          Server: "nodejs",
        });

        done(null, newPlayload);
      },
    },
    (request, reply) => {
      reply.send({ status: "up" });
    }
  );

  fastify.get("/health/alwaysUp", (request, reply) => {
    reply.send({ status: "up" });
  });

  next();
}

export default health;
```

The `onSend` hook has been moved to the route options for the `/health` route, so it only affects the request/response cycle for this route. You can observe the difference by making requests to `/health` and `/health/alwaysUp` as shown below. Even though they are in the same plugin context with identical handlers, the content of their responses is different.

**GET /health**:

```text
HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
server: nodejs
cache-control: no-store
content-length: 17
Date: Sun, 12 Feb 2023 19:08:37 GMT
Connection: keep-alive
Keep-Alive: timeout=72

{"status":"down"}
```

**GET /health/alwaysUp**:

```text
HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
server: fastify
content-length: 15
Date: Sun, 12 Feb 2023 19:09:18 GMT
Connection: keep-alive
Keep-Alive: timeout=72

{"status":"up"}
```

### Fastify Application Hooks

Application hooks, on the other hand, are executed outside of the request/reply lifecycle. They are executed upon events emitted by the `Fastify` instance and can be used to carry out general server or plugin tasks, such as:

- Connecting to databases and other resources
- Loading or unloading configuration
- Closing connections
- Persisting data
- Flushing logs

And others.

Here's an example that simulates a graceful shutdown using Fastify's `onClose` application hook. Such a setup is ideal if you are deploying an update or your server needs to restart for other reasons.

```javascript
fastify.addHook("onClose", (instance, done) => {
  instance.log.info("Server is shutting down...");
  // Perform any necessary cleanup tasks here
  done();
});

process.on("SIGINT", () => {
  fastify.close(() => {
    fastify.log.info("Server has been shut down");
    process.exit(0);
  });
});
```

In this example, the `onClose` hook is registered on the server's root context, and the callback is executed before the server is shut down. The hook function has access to the current `Fastify` instance and a `done` callback, which should be called when the hook is finished.

In addition, the `process.on()` function listens for the `SIGINT` signal, which is sent to the process when you press `CTRL+C` or the system shuts down. When the signal is received, the `fastify.close()` function is called to shut down the server, and a record of the server closure is logged to the console.

After adding the above code to your program, start the server and press `Ctrl-C` to shut down the process. You will observe the following logs in the console:

```json
{"level":30,"time":1676240333903,"pid":615734,"hostname":"fedora","msg":"Server is shutting down..."}
{"level":30,"time":1676240333903,"pid":615734,"hostname":"fedora","msg":"Server has been shut down"}
```

### Middleware in Fastify

Fastify also supports Express-style middleware but it requires you to install an external plugin such as [@fastify/express](https://github.com/fastify/fastify-express) or [@fastify/middie](https://github.com/fastify/middie). This eases migration from Express to Fastify, but it should not be used in greenfield projects in favor of hooks. Note that in many cases, you can find a native Fastify plugin that provides the same functionality as Express middleware.

The example below demonstrates how to use a standard Express middleware such as [cookie-parser](https://www.npmjs.com/package/cookie-parser) in Fastify, but you should prefer native alternatives — such as [@fastify/cookie](https://www.npmjs.com/package/@fastify/cookie) — where possible, since they are better optimized for use in Fastify.

```javascript
import Fastify from "fastify";
import middie from "@fastify/middie";
import cookieParser from "cookie-parser";

const fastify = Fastify({
  logger: true,
});

await fastify.register(middie);

fastify.use(cookieParser());

fastify.get("/", function (request, reply) {
  console.log("Cookies: ", request.raw.cookies);
  reply.send("Hello world!");
});

const port = process.env.PORT || 3000;

fastify.listen({ port }, function (err, address) {
  if (err) {
    fastify.log.error(err);
    process.exit(1);
  }

  fastify.log.info(`Fastify is listening on port: ${address}`);
});
```

Once the `@fastify/middie` plugin is imported and registered, you can begin to use Express middleware via the `use()` method provided on the `Fastify` instance, and it should just work as shown above. Note that every single middleware applied in this manner will be executed on the `onRequest` hook phase.

## Decorators in Fastify

Decorators are a feature that allow you to customize core objects with any type of JavaScript property, such as functions, objects, or any primitive type. For example, you can use decorators to store custom data or register a new method in the `Fastify`, `Request`, or `Reply` objects through the `decorate()`, `decorateRequest()`, and `decorateReply()` methods, respectively.

This example demonstrates using Fastify's decorators to add
functionality to a web application:

```javascript
import Fastify from 'fastify';
import fastifyCookie from '@fastify/cookie';

const fastify = Fastify({
  logger: true,
});

await fastify.register(fastifyCookie, {
  secret: 'secret',
});

fastify.decorate('authorize', authorize);
fastify.decorate('getUser', getUser);
fastify.decorateRequest('user', null);

async function getUser(token) {
  // imagine the token is used to retrieve a user
  return {
    id: 1234,
    name: 'John Doe',
    email: 'john@example.com',
  };
}

async function authorize(request, reply) {
  const { user_token } = request.cookies;
  if (!user_token) {
    throw new Error('unauthorized: missing session cookie');
  }

  const cookie = request.unsignCookie(user_token);
  if (!cookie.valid) {
    throw new Error('unauthorized: invalid cookie signature');
  }

  let user;
  try {
    user = await fastify.getUser(cookie.value);
  } catch (err) {
    request.log.warn(err);
    throw err;
  }

  request.user = user;
}

fastify.get('/', async function (request, reply) {
  await this.authorize(request, reply);

  reply.send(`Hello ${request.user.name}!`);
});

. . .
```

The above snippet of code decorates two functions on the `fastify` instance. The first one is `getUser()` — it takes a token as a parameter and returns a user object (hardcoded in this example).

The `authorize()` function is defined next. It checks whether the `user_token` cookie is present in the request and validates it. If the cookie is invalid or missing, an error is thrown. Otherwise, the cookie value is used to retrieve the corresponding user with the `getUser()` function, and the result is stored in a `user` property on the request object. If an error occurs while retrieving the user, the error is logged and re-thrown.

While you can add any property to `Fastify`, `Request`, or `Reply` objects, you need to declare them in advance with decorators. This helps the underlying JavaScript engine to optimize handling of these objects.

```shell
curl --cookie "user_token=yes.Y7pzW5FUVuoPD5yXLV8joDdR35gNiZJzITWeURHF5Tg" http://127.0.0.1:3000/
```

```text
Hello John Doe!
```

## Data Validation in Fastify

Data validation is an essential feature of any web application that relies on client data, as it helps to prevent security vulnerabilities caused by malicious payloads and improves the reliability and robustness of an application.

Fastify uses [JSON schema](https://json-schema.org/) to define the validation rules for each route's input payload, which includes the request body, query string, parameters, and headers. The JSON schema is a standard format for defining the structure and constraints of JSON data, and Fastify uses [Ajv](https://www.npmjs.com/package/ajv), one of the fastest and most efficient JSON schema validators available.

To use JSON validation in Fastify, you need to define a schema for each route that expects a payload. You can specify the schema using the standard JSON schema format or the Fastify JSON schema format, which is a more concise and expressive way of expressing the schema.

Here's an example of how to define a JSON schema for a route in Fastify:

```javascript
const bodySchema = {
  type: "object",
  properties: {
    name: { type: "string" },
    age: { type: "number" },
    email: { type: "string", format: "email" },
  },
  required: ["name", "email"],
};

fastify.post(
  "/users",
  {
    schema: {
      body: bodySchema,
    },
  },
  async (request, reply) => {
    const { name, age, email } = request.body;
    reply.send({
      name,
      age,
      email,
    });
  }
);
```

In this example, we define a schema that expects an object with three properties: `name`, `age`, and `email`. The `name` property should be a string, the `age` property should be a number, and the `email` property should be a string in email format. We also specify that `name` and `email` are required properties.

When a client sends a POST request to `/users` with an invalid payload, Fastify automatically returns a '400 Bad Request' response with an error message indicating which property failed the validation. However, if the payload adheres to the schema, the route handler function will be executed with the parsed payload as `request.body`.

Here's an example of a request with an invalid payload (the `email` key is in the wrong format):

```shell
curl --header "Content-Type: application/json" \
  --request POST \
  --data '{"name":"John","age": 44,"email":"john@example"}' \
  http://localhost:3000/users
```

And here's the response produced by Fastify:

```json
{"statusCode":400,"error":"Bad Request","message":"body/email must match format \"email\""}⏎
```

[See the docs to learn more about validation in Fastify](https://www.fastify.io/docs/latest/Reference/Validation-and-Serialization/) and how to customize its behavior to suit your needs.

## Planning an Incremental Migration from Express to Fastify

Incremental migration is an excellent strategy for those who want to switch to a new framework but cannot afford to make the change all at once. By adopting an incremental approach, you can gradually introduce Fastify into your project while still using Express, giving you time to make any necessary changes and ensure a smooth transition.

The first step is to identify the parts of your project that would benefit most from Fastify's features, such as its built-in support for validation and logging, and improved performance over Express. Once you have identified those areas, you can introduce Fastify alongside your existing Express code.

This might involve setting up a separate server instance that handles certain routes or endpoints using Fastify while still using Express for the rest of an application. But you'll probably find it easier to use the [@fastify/express](https://github.com/fastify/fastify-express) plugin to add full Express compatibility to Fastify so that you can use Express middleware and applications as if they are Fastify plugins.

To use the `@fastify/express` plugin, you can install it via `npm` and register it with your Fastify instance:

```javascript
import Fastify from "fastify";
import expressPlugin from "@fastify/express";

const fastify = Fastify({
  logger: true,
});

await fastify.register(expressPlugin);
```

You can then use Express middleware or applications just like in an Express application. For example:

```javascript
import express from "express";
const expressApp = express();

expressApp.use((req, res, next) => {
  console.log("This is an Express middleware");
  next();
});

expressApp.get("/express", (req, res) => {
  res.json({ body: "hello from express" });
});

fastify.use(expressApp);
```

```shell
curl http://localhost:3000/express
```

```json
{ "body": "hello from express" }
```

As you become more comfortable with Fastify, you can start to migrate more and more of your code over, eventually replacing all of your Express-specific code with Fastify. By taking the time to plan and execute a thoughtful migration strategy, you can ensure a smooth transition and ultimately end up with a more efficient, high-performing application.

## Up Next: Migrate to Fastify from Express

We've covered a lot of ground in this tutorial. We hope you've gained a deeper understanding of how Fastify's novel features like hooks and decorators can help you customize and extend your applications more than Express.

In the next and final part of this series, we'll provide a practical guide for migrating to Fastify from Express. We'll cover common migration scenarios, provide tips for optimizing performance and improving security, and share some best practices along the way.

Thanks for reading, and see you next time!

**P.S. If you liked this post, [subscribe to our JavaScript Sorcery list](https://blog.appsignal.com/javascript-sorcery) for a monthly deep dive into more magical JavaScript tips and tricks.**

**P.P.S. If you need an APM for your Node.js app, go and [check out the AppSignal APM for Node.js](https://www.appsignal.com/nodejs).**