DEV Community

Cover image for Migrate Your Express Application to Fastify
Damilola Olatunji for AppSignal

Posted on • Originally published at blog.appsignal.com

Migrate Your Express Application to Fastify

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 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. This application utilizes Express, Mongoose, and Pug to create a URL Shortener app as follows:

URL Shortener Application

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:

git clone https://github.com/damilolaolatunji/node-url-shortener
Enter fullscreen mode Exit fullscreen mode

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

npm install
Enter fullscreen mode Exit fullscreen mode

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

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>
Enter fullscreen mode Exit fullscreen mode

The application is configured to use AppSignal 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 (you can do a 30-day free trial, no credit card required), then create a new application 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:

npm run start:dev
Enter fullscreen mode Exit fullscreen mode

The following output indicates that the server started successfully:

[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"}
Enter fullscreen mode Exit fullscreen mode

You will also observe that your application's logs will start to appear in the logging dashboard in AppSignal:

Application logs in AppSignal

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

Now head over to 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:

.
├── 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
Enter fullscreen mode Exit fullscreen mode

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 of this series, using the @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:

npm install fastify @fastify/express
Enter fullscreen mode Exit fullscreen mode

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

// app.js
import Fastify from "fastify";
import fastifyExpress from "@fastify/express";
Enter fullscreen mode Exit fullscreen mode

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:

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

const fastifyApp = Fastify();

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

export default fastifyApp;
Enter fullscreen mode Exit fullscreen mode

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:

//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}`
  );

. . .
Enter fullscreen mode Exit fullscreen mode

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:

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

export default router;
Enter fullscreen mode Exit fullscreen mode

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:

// routes/routes.js
router.get("/", rootController.render);
Enter fullscreen mode Exit fullscreen mode

Next, register the / route in fastifyRoutes as follows:

// routes/routes.js
export async function fastifyRoutes(fastify) {
  fastify.get("/", rootController.render);
}
Enter fullscreen mode Exit fullscreen mode

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

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

export default { render };
Enter fullscreen mode Exit fullscreen mode

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:

// app.js
expressApp.set("view engine", "pug");
expressApp.set("views", path.join(__dirname, "views"));
Enter fullscreen mode Exit fullscreen mode

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 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:

npm install @fastify/view
Enter fullscreen mode Exit fullscreen mode

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

// app.js
. . .
import fastifyView from '@fastify/view';
import pug from 'pug';
Enter fullscreen mode Exit fullscreen mode

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

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

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

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:

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

. . .

await fastifyApp.register(fastifyRoutes);
Enter fullscreen mode Exit fullscreen mode

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

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

export default { render };
Enter fullscreen mode Exit fullscreen mode

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:

// 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);
}
Enter fullscreen mode Exit fullscreen mode

Since Fastify supports schema validation with 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:

// 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 };
Enter fullscreen mode Exit fullscreen mode

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

// app.js
import routes from "./routes/routes.js";
Enter fullscreen mode Exit fullscreen mode

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

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

. . .
expressApp.use(routes);
Enter fullscreen mode Exit fullscreen mode

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

// app.js
await fastifyApp.register(routes);
Enter fullscreen mode Exit fullscreen mode

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:

// app.js
expressApp.use("/static", express.static(path.join(__dirname, "public")));
Enter fullscreen mode Exit fullscreen mode

We can replace this middleware with the @fastify/static plugin, which you can install through the command below:

npm install @fastify/static
Enter fullscreen mode Exit fullscreen mode

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

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

. . .

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

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:

Install the plugins through the command below:

npm install @fastify/helmet @fastify/cors @fastify/compress
Enter fullscreen mode Exit fullscreen mode

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

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

. . .

expressApp.use(helmet());
expressApp.use(cors());
expressApp.use(compression());
Enter fullscreen mode Exit fullscreen mode

Then add the following lines in the appropriate location:

// 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);
. . .
Enter fullscreen mode Exit fullscreen mode

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

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 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:

// 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;
Enter fullscreen mode Exit fullscreen mode

We also install and import the 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:

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

. . .

expressApp.use(expressErrorHandler());
expressApp.use(errorHandler);
Enter fullscreen mode Exit fullscreen mode

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:

function (error, request, reply) {}
Enter fullscreen mode Exit fullscreen mode

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

// 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;
Enter fullscreen mode Exit fullscreen mode

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:

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

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

  fastify.setErrorHandler(errorHandler);
}
Enter fullscreen mode Exit fullscreen mode

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

You can learn more about error handling in Fastify's documentation and on AppSignal's error tracking page.

Replacing Winston Logging with Pino

Our Express application uses Winston and Morgan for logging HTTP requests.

Fastify comes with Pino by default, but logging must be enabled for it to work:

// app.js
const fastifyApp = Fastify({
  logger: true,
});
Enter fullscreen mode Exit fullscreen mode

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

{"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"}
Enter fullscreen mode Exit fullscreen mode

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:

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

expressApp.use(requestLogger);
Enter fullscreen mode Exit fullscreen mode

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:

// app.js

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

const expressApp = express();

. . .
await fastifyApp.register(fastifyExpress);
fastifyApp.use(expressApp);
Enter fullscreen mode Exit fullscreen mode

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

// 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;
Enter fullscreen mode Exit fullscreen mode

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

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

. . .

const fastifyApp = Fastify({
  logger: logger,
});
Enter fullscreen mode Exit fullscreen mode

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

{"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"}
Enter fullscreen mode Exit fullscreen mode

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:

// 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 };
Enter fullscreen mode Exit fullscreen mode

Learn more about logging in Fastify and how to customize the Pino logger.

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:

npm uninstall express body-parser express-async-errors helmet winston cors @fastify/express express-json-validator-middleware ajv-formats compression morgan
Enter fullscreen mode Exit fullscreen mode

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

rm src/middleware/validator.js src/middleware/requestLogger.js
Enter fullscreen mode Exit fullscreen mode

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

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 to learn more. Also, you can find all the code samples we used in this GitHub repository.

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

Top comments (0)