Migrate Your Express Application to Fastify
Tags: node, express, fastify
Published: 2023-07-05 11:46:03

Welcome to the final part of our Express to Fastify series. In the previous installments, we explored the unique features and advantages of Fastify over Express. Now, you'll put what you've learned so far into practice by migrating an existing Express application to Fastify.

You'll avoid rewriting an entire application from scratch by gradually transitioning to Fastify. After reading this article, you'll have the skills to confidently migrate your Express applications and enjoy the benefits of the Fastify framework.

Let's get started!

## Prerequisites

Before proceeding with the article, ensure that you have a recent version of Node.js and npm installed on your computer. You also need to [install MongoDB](https://www.mongodb.com/docs/manual/administration/install-community/) and ensure that it is running on port 27017.

## Setting Up the Demo Project

To illustrate the process of migrating from Express to Fastify, we have prepared a [demo application](https://github.com/damilolaolatunji/node-url-shortener). This application utilizes Express, [Mongoose](https://mongoosejs.com/docs/), and [Pug](https://pugjs.org/) to create a URL Shortener app as follows:

![URL Shortener Application](https://blog.appsignal.com/images/blog/2023-06/url-shortener.png)

After you submit a URL through the form, the application generates a shortened URL that can be copied and used elsewhere.

Go ahead and clone the repository to your computer using the following command:

```shell
git clone https://github.com/damilolaolatunji/node-url-shortener
```

Afterward, `cd` into the project directory and install the application dependencies with `npm`:

```shell
npm install
```

Once the process completes, rename the `.env.sample` file to `.env` and edit it as follows:

```text
NODE_ENV=development
LOG_LEVEL=debug
PORT=3000
MONGODB_URL=mongodb://127.0.0.1:27017/url_shortener
APPSIGNAL_PUSH_API_KEY=<your_appsignal_push_api_key>
```

The application is configured to use [AppSignal](https://www.appsignal.com/) for log management and error tracking, so you can easily monitor and troubleshoot any issues that may occur. AppSignal provides real-time alerts and detailed error reports, allowing you to quickly identify and fix the root cause of errors before they become major problems.

If you want to see this in action, [sign up for an AppSignal account](https://appsignal.com/users/sign_up) (you can do a 30-day free trial, no credit card required), then [create a new application](https://docs.appsignal.com/guides/new-application.html) and copy the **Push API Key** under **Push & Deploy** in the **APP SETTINGS** page.

Once you've done that, replace the `<your_appsignal_push_api_key>` placeholder in the `.env` file. Note that this step is not required to complete the migration to Fastify.

You can then start the development server by executing the command below:

```shell
npm run start:dev
```

The following output indicates that the server started successfully:

```text
[nodemon] starting `node src/index.js`
{"level":"info","message":"Connected to MongoDB","timestamp":"2023-05-03T12:26:07.746Z"}
{"level":"info","message":"URL Shortener is running in development mode → PORT 3000","timestamp":"2023-05-03T12:26:07.753Z"}
```

You will also observe that [your application's logs](https://www.appsignal.com/tour/log-management) will start to appear in the logging dashboard in AppSignal:

![Application logs in AppSignal](https://blog.appsignal.com/images/blog/2023-06/appsignal-logs.png)

From here, you can perform all kinds of actions to monitor and troubleshoot your application.

Now head over to [http://localhost:3000](http://localhost:3000) to see the application in action!

## Examining the Project Structure

The URL Shortener application is composed of several files and directories as shown below:

```text
.
├── package.json
├── package-lock.json
├── README.md
└── src
    ├── app.js
    ├── appsignal.js
    ├── config
    │   ├── config.js
    │   └── logger.js
    ├── controllers
    │   ├── root.controller.js
    │   └── url.controller.js
    ├── index.js
    ├── middleware
    │   ├── error.js
    │   ├── requestLogger.js
    │   └── validator.js
    ├── models
    │   ├── plugins
    │   │   └── toJSON.plugin.js
    │   └── url.model.js
    ├── public
    │   ├── main.js
    │   └── style.css
    ├── routes
    │   └── routes.js
    ├── schemas
    │   └── url.schema.js
    └── views
        ├── default.pug
        └── home.pug
```

Here's a brief explanation of the most important ones:

- `src/index.js`: The entry point of the application.
- `src/app.js`: Where the Express app is configured.
- `src/appsignal.js`: Initializes AppSignal integration.
- `src/public/`: Contains static CSS and JavaScript files.
- `src/views/`: Contains the Pug templates for the app.
- `src/routes/routes.js`: Configures the application routes.
- `src/config/`: Contains configuration for the Winston logger and environment variables.
- `src/models/`: Has the schema definitions for Mongoose.
- `src/controllers/`: Includes the business logic for the application routes.
- `src/middleware/`: Contains some useful middleware functions.

We'll examine most of these files' content as we migrate our application to Fastify.

In the next section, we'll install Fastify in the project and create a new Fastify application that embeds the original Express app to kickstart our incremental migration.

## Migrating from Express to Fastify

As mentioned in [part 2](https://blog.appsignal.com/2023/05/24/advanced-fastify-hooks-middleware-and-decorators.html) of this series, using the [@fastify/express](https://github.com/fastify/fastify-express) plugin is the quickest way to get your existing Express application working with Fastify. The plugin adds full Express compatibility to Fastify so that you can easily use any Express middleware — or even an entire Express application — with your Fastify instance, and it will just work with no changes required.

Go ahead and install the `fastify` and `@fastify/express` packages into your project:

```shell
npm install fastify @fastify/express
```

Once they are both installed, import them at the start of your `app.js` file as follows:

```javascript
// app.js
import Fastify from "fastify";
import fastifyExpress from "@fastify/express";
```

Afterward, create a new Fastify application instance near the bottom of the file and register the `fastifyExpress` plugin. Also, register the `expressApp` on the Fastify instance, then change the file export from `expressApp` to `fastifyApp`:

```javascript
// app.js
. . .
expressApp.use(expressErrorHandler());
expressApp.use(errorHandler);

const fastifyApp = Fastify();

await fastifyApp.register(fastifyExpress);
fastifyApp.use(expressApp);

export default fastifyApp;
```

At this point, `app.js` exposes a Fastify application that embeds a fully-featured Express application with its own routes, middleware, and plugins.

Before testing your changes, you also need to modify your `index.js` file as shown below. You only need to change the part of the code that listens for connections on the configured port:

```javascript
//app.js
. . .

  logger.info('Connected to MongoDB');

  const address = await app.listen({ port: config.port });
  logger.info(
    `URL Shortener is running in ${config.env} mode → PORT ${address}`
  );

. . .
```

With these changes in place, your application should continue working as before.

Fastify is now being used to start the server, but the entire service functionality remains in the Express application. In the next section, we'll start migrating our routes to Fastify.

## Migrating Express Routes to Fastify

The Express router currently does our application routing, but we'll start migrating the routes to Fastify in this section. Open your `routes/routes.js` file and add the following code:

```javascript
// routes/routes.js
. . .
export async function fastifyRoutes(fastify) {
}

export default router;
```

The `fastifyRoutes` function represents a new plugin context that will contain our application routes. To migrate an Express route to Fastify, you need to:

1. Remove the route from the `express.Router` instance.
2. Register the route on the `fastify` instance in the plugin function.
3. Update the route itself so that it conforms to the Fastify API.

Let's follow the above steps for the `/` route and see how far we get.

First, delete the following line of code in the `routes.js` file:

```javascript
// routes/routes.js
router.get("/", rootController.render);
```

Next, register the `/` route in `fastifyRoutes` as follows:

```javascript
// routes/routes.js
export async function fastifyRoutes(fastify) {
  fastify.get("/", rootController.render);
}
```

Afterward, open the `controllers/root.controller.js` and examine its content:

```javascript
// controller/root.controller.js
function render(req, res) {
  res.render("home");
}

export default { render };
```

This route renders the `src/views/home.pug` file and returns the resulting HTML string to the client. In the `app.js` file, Express is configured to work with Pug templates through the following lines of code:

```javascript
// app.js
expressApp.set("view engine", "pug");
expressApp.set("views", path.join(__dirname, "views"));
```

Since we've migrated our only application route that renders templates, the above lines are no longer needed and can be safely deleted. But you now need to configure Fastify to render the Pug template through the [@fastify/view](https://github.com/fastify/point-of-view) plugin. It decorates the `Reply` interface with a method that we'll then use to render the template.

Go ahead and install the plugin first:

```shell
npm install @fastify/view
```

Then import it into the `app.js` file shown below. You also need to import `pug` as well:

```javascript
// app.js
. . .
import fastifyView from '@fastify/view';
import pug from 'pug';
```

Below the `fastifyApp` declaration, add the following lines to register the `fastifyView` plugin:

```javascript
// app.js
. . .
const fastifyApp = Fastify();

await fastifyApp.register(fastifyView, {
  engine: {
    pug,
  },
  root: path.join(__dirname, 'views'),
  propertyName: 'render',
  viewExt: 'pug',
});
. . .
```

At this point, your Fastify application is now ready to work with Pug templates. You only need to register the `fastifyRoutes` plugin in your `fastifyApp` instance as shown below:

```javascript
// app.js
import routes, { fastifyRoutes } from './routes/routes.js';

. . .

await fastifyApp.register(fastifyRoutes);
```

You can also update the `rootController.render` method as follows so that it conforms with Fastify's naming convention:

```javascript
function render(req, reply) {
  reply.render("home");
}

export default { render };
```

With the above changes in place, the application should
continue working as before. The `/` route is handled by Fastify, while the other two routes remain routed through Express. In this manner, you can eventually migrate all your Express routes to Fastify.

## Migrating the Rest of the Express Routes

In this section, we'll migrate the remaining two Express routes to Fastify. Start by updating the `routes.js` file as follows:

```javascript
// routes/routes.js
import urlSchema from "../schemas/url.schema.js";
import urlController from "../controllers/url.controller.js";
import rootController from "../controllers/root.controller.js";

export default async function fastifyRoutes(fastify) {
  fastify.get("/", rootController.render);

  fastify.post(
    "/shorten",
    {
      schema: {
        body: urlSchema,
      },
    },
    urlController.shorten
  );

  fastify.get("/:shortID", urlController.redirect);
}
```

Since Fastify supports schema validation with [Ajv](https://www.npmjs.com/package/ajv), the `validate` module is no longer required on the `/shorten` route, and we can specify the JSON schema directly on the route. The controllers for both routes will largely remain the same, except that the `res` parameter is renamed to `reply` as before:

```javascript
// controllers/url.controller.js
async function shorten(req, reply) {
  const { shortID, destination } = await Url.findOrCreate(req.body.destination);

  logger.debug(`${destination} shortened to ${shortID}`, {
    shortID,
    destination,
  });

  reply.send({ shortID, destination });
}

async function redirect(req, reply) {
  const { shortID } = req.params;

  const { destination } = await Url.findOne({ shortID });

  logger.debug(`redirecting /${shortID} to ${destination}`, {
    shortID,
    destination,
  });

  reply.redirect(destination);
}

export default { shorten, redirect };
```

Finally, head back to the `app.js` file and fix the `routes` import since `fastifyRoutes` is now the default export:

```javascript
// app.js
import routes from "./routes/routes.js";
```

Now all our Express routes have been migrated over to Fastify. You can safely remove these lines:

```javascript
// app.js
expressApp.use(express.json());
expressApp.use(express.urlencoded({ extended: true }));

. . .
expressApp.use(routes);
```

Then replace the reference to `fastifyRoutes` with `routes` as shown below:

```javascript
// app.js
await fastifyApp.register(routes);
```

At this stage, you can test the application once again to confirm that it works just fine as before.

We've now successfully moved all our routes to Fastify without much trouble. In the next section, we will shift our handling of static files from Express to Fastify.

## Serving Static Files with Fastify

At the moment, the static JavaScript and CSS files (located in `/src/public`) are being handled by the `express.static` middleware, according to this line in `app.js`:

```javascript
// app.js
expressApp.use("/static", express.static(path.join(__dirname, "public")));
```

We can replace this middleware with the [@fastify/static](https://github.com/fastify/fastify-static) plugin, which you can install through the command below:

```shell
npm install @fastify/static
```

Once installed, import it into the file and register it on the `fastifyApp` instance:

```javascript
// app.js
import fastifyStatic from '@fastify/static';

. . .

await fastifyApp.register(fastifyStatic, {
  root: path.join(__dirname, 'public'),
  prefix: '/static/',
});
```

Then remove the reference to `express.static`, and everything should keep working the same as before (except that Fastify now handles serving static files).

## Replacing Express Middleware with Fastify Plugins

We've almost completed the migration to Fastify, but we still have some Express bits to replace before removing the `@fastify/express` compatibility layer. In this section, we'll replace the following middleware with the corresponding [Fastify alternatives](https://www.fastify.io/ecosystem/):

- [helmet](https://www.npmjs.com/package/helmet) ->
  [@fastify/helmet](https://github.com/fastify/fastify-helmet).
- [compression](https://www.npmjs.com/package/compression) ->
  [@fastify/compress](https://github.com/fastify/fastify-compress).
- [cors](https://www.npmjs.com/package/cors) ->
  [@fastify/cors](https://github.com/fastify/fastify-cors).

Install the plugins through the command below:

```shell
npm install @fastify/helmet @fastify/cors @fastify/compress
```

In your `app.js` file, delete the following lines:

```javascript
// app.js
import helmet from 'helmet';
import cors from 'cors';
import compression from 'compression';

. . .

expressApp.use(helmet());
expressApp.use(cors());
expressApp.use(compression());
```

Then add the following lines in the appropriate location:

```javascript
// app.js
. . .
import fastifyCors from '@fastify/cors';
import fastifyHelmet from '@fastify/helmet';
import fastifyCompress from '@fastify/compress';

. . .

await fastifyApp.register(fastifyCors);
await fastifyApp.register(fastifyHelmet);
await fastifyApp.register(fastifyCompress);
. . .
```

You can confirm that these work by making a request to the application root and inspecting the response headers through your browser DevTools.

![Confirming that Fastify Plugins work](https://blog.appsignal.com/images/blog/2023-06/fastify-plugin-check.png)

At this point, we've replaced most of the Express middleware with Fastify plugins without affecting application functionality. What if you're unable to find a Fastify alternative to your Express middleware? You can use the [techniques discussed in part 2 of this series](https://blog.appsignal.com/2023/05/24/advanced-fastify-hooks-middleware-and-decorators.html) to continue using the
middleware even when the rest of the application has been migrated to Fastify.

In the next section, we'll turn our attention to the error handling middleware.

## Error Handling in Fastify

In Express, error handling is done through middleware functions added to an application's middleware stack. These functions have an additional `err` parameter denoting the error being handled, and they are called when an error occurs anywhere in the middleware stack or is thrown in a route handler. The error handler for our Express app is located in `middleware/error.js`:

```javascript
// middleware/error.js
import { ValidationError } from "express-json-validator-middleware";
import logger from "../config/logger.js";

const errorHandler = (err, req, res, next) => {
  let statusCode = 500;
  let message = "internal server error";

  if (err instanceof ValidationError) {
    statusCode = 400;
    message = "validation error";
  } else {
    logger.error(err);
  }

  const response = {
    code: statusCode,
    message,
  };

  res.status(statusCode).send(response);
};

export default errorHandler;
```

We also install and import the [express-async-errors](https://www.npmjs.com/package/express-async-errors) package so that async errors are caught by this middleware instead of crashing the program.

Finally, we use AppSignal's `expressErrorHandler` middleware to automatically track unexpected errors:

```javascript
// app.js
. . .
import 'express-async-errors';
import { expressErrorHandler } from '@appsignal/nodejs';
import errorHandler from './middleware/error.js';

. . .

expressApp.use(expressErrorHandler());
expressApp.use(errorHandler);
```

Let's begin migrating our error handling to Fastify by removing the above lines in the `app.js` file. By default, Fastify automatically catches errors from both synchronous and asynchronous routes, so no plugin replacement for `express-async-errors` is needed. You can customize the default error-handling behavior in Fastify by providing a custom error handler through the `setErrorHandler()` method on a `Fastify` instance. The provided handler needs to have the following signature:

```javascript
function (error, request, reply) {}
```

Let's modify our existing Express error handler middleware so that it can handle Fastify errors instead:

```javascript
// middleware/error.js
import logger from "../config/logger.js";
import { sendError } from "@appsignal/nodejs";

function errorHandler(err, req, reply) {
  let statusCode = 500;
  let message = "internal server error";

  if (err.code === "FST_ERR_VALIDATION") {
    statusCode = 400;
    message = "validation error";
    logger.info(err);
  } else {
    sendError(err);
    logger.error(err);
  }

  const response = {
    code: statusCode,
    message,
  };

  reply.code(statusCode).send(response);
}

export default errorHandler;
```

The code remains mostly the same, except that the function signature has been modified, and schema validation errors are now detected differently. Also, the `sendError()` method replaces `expressErrorHandler` as the way to track unexpected application errors in AppSignal.

You may now set this function as the error handler for your routes using the `setErrorHandler()` method:

```javascript
// routes/routes.js
. . .
import errorHandler from '../middleware/error.js';

export default async function fastifyRoutes(fastify) {
  . . .

  fastify.setErrorHandler(errorHandler);
}
```

At this point, any error that occurs in your routes will be caught and processed by the custom `errorHandler` you provided. They'll also be tracked in AppSignal so that you can be notified quickly of any issues that occur in your Node.js application.

![Node.js errors in AppSignal](https://blog.appsignal.com/images/blog/2023-06/appsignal-errors.png)

You can learn more about [error handling in Fastify's documentation](https://www.fastify.io/docs/latest/Reference/Errors/) and on [AppSignal's error tracking page](https://www.appsignal.com/tour/errors).

## Replacing Winston Logging with Pino

Our Express application uses [Winston](https://www.npmjs.com/package/winston) and [Morgan](https://www.npmjs.com/package/morgan) for logging HTTP requests.

Fastify comes with [Pino](https://github.com/pinojs/pino) by default, but logging must be enabled for it to work:

```javascript
// app.js
const fastifyApp = Fastify({
  logger: true,
});
```

When Fastify logging is enabled, you'll notice that Pino logs appear alongside Winston logs when requests are sent to the server:

```json
{"level":30,"time":1683531253228,"pid":782352,"hostname":"fedora","reqId":"req-2","res":{"statusCode":304},"responseTime":6.965402990579605,"msg":"request completed"}
{"level":30,"time":1683531253229,"pid":782352,"hostname":"fedora","reqId":"req-3","res":{"statusCode":304},"responseTime":4.585820972919464,"msg":"request completed"}
{"content_length":"0","level":"info","message":"GET /static/main.js 304 0 - 2.775 ms","method":"GET","path":"/static/main.js","response_time_ms":"2.775","status_code":304,"timestamp":"2023-05-08T07:34:13.229Z","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/113.0"}
```

At this point, you can remove the `requestLogger` from the Express application so that only Fastify logs remain. Go ahead and delete the following lines of code from your `app.js` file:

```javascript
// app.js
import requestLogger from "./middleware/requestLogger.js";

expressApp.use(requestLogger);
```

At this stage, we've completely migrated the Express application over to Fastify so you can remove the `express` and `@fastify/express` packages as well:

```javascript
// app.js

import express from 'express';
import fastifyExpress from '@fastify/express';
. . .

const expressApp = express();

. . .
await fastifyApp.register(fastifyExpress);
fastifyApp.use(expressApp);
```

Head to your `config/logger.js` file, and replace `winston` with `pino` as shown below:

```javascript
// config/logger.js
import pino from "pino";
import config from "./config.js";

const logger = pino({
  level: config.logLevel,
  formatters: {
    bindings: (bindings) => {
      return { pid: bindings.pid, host: bindings.hostname };
    },

    level: (label) => {
      return { level: label };
    },
  },
  timestamp: pino.stdTimeFunctions.isoTime,
});

export default logger;
```

Afterward, replace Fastify's default logger instance with the custom Pino logger:

```javascript
// app.js
import logger from './config/logger.js';

. . .

const fastifyApp = Fastify({
  logger: logger,
});
```

At this point, all application logs are now produced through the same Pino instance, so the log formatting should be consistent:

```json
{"level":"info","time":"2023-05-08T08:09:43.047Z","pid":920874,"host":"fedora","reqId":"req-1","req":{"method":"POST","url":"/shorten","hostname":"localhost:3000","remoteAddress":"127.0.0.1","remotePort":60582},"msg":"incoming request"}
{"level":"debug","time":"2023-05-08T08:09:43.058Z","pid":920874,"host":"fedora","msg":"https://github.com/nvim-telescope/telescope.nvim#themes shortened to mB5Ke86"}
{"level":"info","time":"2023-05-08T08:09:43.060Z","pid":920874,"host":"fedora","reqId":"req-1","res":{"statusCode":200},"responseTime":12.570792019367218,"msg":"request completed"}
```

Note that Fastify makes its logger available on the `Server` and `Request` types so you can access it directly without importing from `config/logger.js`. For example:

```javascript
// controllers/url.controller.js
import Url from "../models/url.model.js";

async function shorten(req, reply) {
  const { shortID, destination } = await Url.findOrCreate(req.body.destination);

  req.log.debug(`${destination} shortened to ${shortID}`);

  reply.send({ shortID, destination });
}

async function redirect(req, reply) {
  const { shortID } = req.params;

  const { destination } = await Url.findOne({ shortID });

  req.log.debug(`redirecting /${shortID} to ${destination}`);

  reply.redirect(destination);
}

export default { shorten, redirect };
```

Learn more about [logging in Fastify](https://www.fastify.io/docs/latest/Reference/Logging) and [how to customize the Pino logger](https://getpino.io/).

## Testing Your Application

We've successfully completed our migration from Express to Fastify! You may now remove all the Express-related packages from your project through the command below:

```shell
npm uninstall express body-parser express-async-errors helmet winston cors @fastify/express express-json-validator-middleware ajv-formats compression morgan
```

You can also remove some of the files that are no longer needed:

```shell
rm src/middleware/validator.js src/middleware/requestLogger.js
```

Afterward, ensure you thoroughly test your application to confirm that it's working as expected, ideally through automated tests that you've previously set up. However, since our application is small and we don't have any automated tests, manual testing will suffice:

![Fastify application](https://blog.appsignal.com/images/blog/2023-06/fastify-app.png)

## Wrapping Up

Thank you for following along with this series on migrating from Express to Fastify. We've covered a lot of ground, and I hope you found it helpful in deciding whether to switch to Fastify and how to approach the migration process.

Fastify offers many additional features that we couldn't cover in this series, so check out the [Fastify official documentation](https://www.fastify.io/docs/latest/) to learn more. Also, you can find all the code samples we used in [this GitHub repository](https://github.com/damilolaolatunji/node-url-shortener).

I hope this series has equipped you with the knowledge and skills to start building performant and scalable web applications with Fastify. If you have any questions or feedback, please don't hesitate to reach out.

Until next time, happy coding!

**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).**