Categories
Fastify Node.js

How to migrate your app from Express to Fastify

12 min read

Express has long been the most popular framework for developing web applications with Node.js. Unfortunately, this framework hasn’t seen much active development in recent years. This means that it doesn’t have support for modern JavaScript features. In the meantime, a number of new frameworks have emerged which take a different approach to Node.js application development. One of these frameworks is Fastify.

In this article, we’ll look at what makes Fastify an appealing alternative for developing web applications with Node.js. We’ll learn how we can avoid the need to rewrite our existing Express applications from scratch, and instead migrate them to using Fastify in phases. By the time we’re done, you’ll be able to confidently migrate your existing Express applications and start leveraging the benefits of the Fastify framework.

There are a few requirements for following along with this article:

  • You’ll need to be comfortable with creating a basic Express application, defining routes and configuring middleware.
  • You’ll need to be comfortable running commands in a terminal.
  • You’ll need to have Node.js >= v14.13.0 installed. This provides us with good support for ECMAScript (ES) modules and allows us to use top-level await. The code examples in this article use ES module syntax (import / export).

All of the example code in this article is available on GitHub for you to browse, download and experiment with.

🎬 There is a video version of this article — watch it here.

Jump links

What are the benefits of migrating from Express to Fastify?

If you’re comfortable building Node.js applications with Express, you might be wondering what the benefits are of migrating existing Express applications to Fastify. Here are some great reasons to consider making the move:

  • Validation and logging out of the box. These features are commonly required when building web applications. When using Fastify, there’s no need to choose and integrate libraries for these tasks, as it provides them for us. We’ll learn more about these features later in this article.

  • Native support for async code. Fastify natively handles promises and supports async / await. This means that routes will catch uncaught rejected promises for us. This allows us to write asynchronous code safely. It also lets us do neat things, like automatically send the return value from a route handler function as the response body:

app.get("/user/:id", async (request) => await getUser(request.params.id));
  • Automatic parsing and serialization of JSON. We don’t need to configure Fastify to parse JSON request bodies, or to serialize objects as JSON for responses. It handles all of this automatically for us:
app.get("/user/:id", async (request, reply) => {
  const name = request.body.name;
  
  reply.send({ user: { name } });
});
  • Developer friendly. With explicit and expressive APIs, as well as excellent support for TypeScript, Fastify has been designed with developer experience in mind.

  • It’s fast. We never want a framework to become the source of performance bottlenecks in our applications. The good news is that Fastify has been built to be highly performant. The Fastify benchmarks show how it compares against other Node.js web frameworks.

  • In active development. The Fastify framework is being actively developed. There are regular releases with improvements and bug/security fixes.

How to Migrate an API with Confidence

We want to be confident that our application is still working as expected after it has been migrated to Fastify. One of the things which can help us catch bugs or identify unintended changes is API integration tests.

Integration tests exercise the components of an application in a different way to unit tests. Unit tests exercise the functions of individual components on their own. Integration tests allow us to verify the behavior of multiple components working together.

If we write API integration tests for an Express application, we want to be able to run those same tests once we’ve migrated the application to Fastify. When writing integration tests for an API, there are a few key things to consider:

  • They shouldn’t be tied to a specific framework. We want to be able to run the same tests before and after migration, without the need to change the tests or any of the libraries that we’re using for them.

  • Keep them simple. At a minimum, the integration tests should make requests to the endpoints which an API exposes and verify that a response is returned, but generally not much more. We might want to check for specific HTTP status codes or response headers, but we should try to keep the tests as simple as possible.

  • Pick tools you’re comfortable with. There are lots of different tools which can help us with creating and running API tests, but it’s important to use tools which we’re comfortable with. To write effective integration tests, we need to be able to make HTTP requests and make assertions against the responses from our API. In general, we don’t need a lot of libraries or tools to make this work.

We won’t be digging into the details of how to implement API integration tests in this article, but they’re something you should consider writing before undertaking a framework migration.

Transitioning from Express to Fastify with fastify-express

The idea of migrating an existing Express application to a completely different framework can seem quite daunting. Fortunately, the Fastify team have created a plugin — fastify-express — which can help ease the migration path.

The fastify-express plugin adds full Express compatibility to Fastify. It provides a use() method which we can use to add Express middleware and routes to our Fastify server. This gives us the option of gradually migrating parts of an existing Express application over to Fastify.

Here’s an example of Express router:

// src/routes.js
const router = express.Router();

router.get("/:user_id", function getUser(request, response, next) {
  response.json({});
});

export default router;

We can then use fastify-express to add our existing Express router to a Fastify server instance:

// src/server.js

import Fastify from "fastify";
import ExpressPlugin from "fastify-express";

import routes from "./routes.js";

const fastify = Fastify();

await fastify.register(ExpressPlugin);

fastify.use("/user", routes);

await fastify.listen(3000);

We’ll explore the details of how this all works when we start migrating our application to Fastify a little later.

It’s important to be aware that using the fastify-express plugin is not a long-term solution. If we want to get the full benefits of Fastify, we’ll need to migrate our Express-specific application code at some point. However, the fastify-express plugin provides us with the opportunity for a phased migration to Fastify.

Our Example Express Application

We’re going to build an example Express application and then migrate it to use the Fastify framework. Let’s take a look at the code for it now.

Required dependencies

First, let’s create a new project:

mkdir express-to-fastify-migration
cd express-to-fastify-migration
npm init -y

Then, we’ll run this command in our terminal to install the dependencies which our Express application will require:

npm install express cors

Finally, open up package.json and add the following line above the scripts section:

"type": "module",

This will allow us to use ES modules in our application.

The router module

We’re going to create an Express router instance to help us encapsulate our routes and middleware. Routers in Express can be used to help us organize our application into discrete modules. For example, we might have one router for /user routes and another router for /address routes. We’ll see later how this can help us migrate our Express application to Fastify in stages.

Let’s create a router instance and add some middleware to it:

// src/routes.js

import express from "express";
import cors from "cors";

const router = express.Router();

router.use(express.json());

router.use(cors({ origin: true }));

In the code above, we’ve configured two examples of Express middleware:

  • express.json(). This middleware function is built in to Express. It handles parsing JSON request bodies.
  • cors. This middleware helps us add CORS headers to our API responses. It will allow our API to be called from a web page.

These middleware tools will be run for any requests which are made to routes that we define on this router.

Now that we’ve configured the middleware, we can add the first route to our router:

// src/routes.js

router.post("/", function createUser(request, response, next) {
  const newUser = request.body;

  if (!newUser) {
    return next(new Error("Error creating user"));
  }

  response.status(201).json(newUser);
});

In a real application, the route handler function above would validate the data which it has received, and then call a database to create a new user record. For this example, we’re sending the data we’ve received as the response body.

Now we’ll add a route for retrieving a user:

// src/routes.js

router.get("/:user_id", function getUser(request, response, next) {
  const user = {
    id: request.params.user_id,
    first_name: "Bobinsky",
    last_name: "Oso",
  };

  response.json(user);
});

As with the POST route, the route handler above would normally make a call to a database to retrieve the user data, but for this example we’ve hard coded an object to send in the response body.

Lastly we’ll export the router object so that we can import it in another module:

// src/routes.js

export default router;

The app module

Now we’re going to create an app module:

// src/app.js

import express from "express";

import routes from "./routes.js";

export default function buildApp() {
  const app = express();

  app.use("/user", routes);

  return app;
}

In this module, we’re defining a function which creates a new Express server instance. We then add our router object to the server instance.

The server module

Lastly, we’ll create a server module. This module uses the buildApp() function we defined in our app module to create a new Express server instance. It then starts our Express server by configuring it to listen on port 3000:

// src/server.js

import buildApp from "./app.js";

const express = buildApp();

express.listen(3000, () => {
  console.log("Example app listening at http://localhost:3000");
});

Running our application

We now have a complete functioning Express application that we can run in our terminal:

node src/server.js

In a separate terminal, we can make a request to the API with cURL to confirm that it’s working:

curl --verbose --request GET \
  --url http://localhost:3000/user/3d395cb4-531c-4989-b8ed-9cc75198187e \
  --header 'Origin: http://example-origin.com'

We should receive a response which looks like this:

< HTTP/1.1 200 OK
< X-Powered-By: Express
< Access-Control-Allow-Origin: http://example-origin.com
< Vary: Origin
< Content-Type: application/json; charset=utf-8
< 

{"id":"3d395cb4-531c-4989-b8ed-9cc75198187e","first_name":"Bobinsky","last_name":"Oso"}

Migrating Our Application from Express to Fastify

Now that we have a fully functional Express application, we're going to migrate it to use the Fastify framework.

Required dependencies

We need to install three dependencies:

Let's run this command in our terminal to install them:

npm install fastify fastify-express fastify-cors

Refactoring our app module

Now that we have our dependencies installed, we need to refactor our app module. We're going to change it to:

  • import fastify and fastify-express instead of express
  • create a Fastify server instance instead of an Express server instance
  • use the fastify-express plugin to add our Express router object to the server

This is what it looks like after we've made those changes:

// src/app.js

import Fastify from "fastify";
import ExpressPlugin from "fastify-express";

import routes from "./routes.js";

export default async function buildApp() {
  const fastify = Fastify({
    logger: true,
  });

  await fastify.register(ExpressPlugin);

  fastify.use("/user", routes);

  return fastify;
}

You can view the diff of these code changes on GitHub.

You'll notice in the code above that we're passing the logger option when we create our Fastify server instance. This enables Fastify's built-in logging functionality. We'll learn more about this later on.

Refactoring our server module

We now need to change our server module to work with a Fastify server instance:

// src/server.js

import buildApp from "./app.js";

const fastify = await buildApp();

try {
  await fastify.listen(3000);
} catch (error) {
  fastify.log.error(error);
  process.exit(1);
}

You can view the diff of these code changes on GitHub.

As Fastify has native support for promises, in the code above we're able to use await and then catch and log any errors with Fastify's built-in logging functionality.

Next steps

Our application is now using Fastify to route requests and send responses. It's fully functional, but Express is still being used by our routes. In order to fully migrate away from Express, we need to migrate our routes to use Fastify as well.

Refactoring our routes module

The routes in our Express application are encapsulated in an Express router. We're going to refactor this router into a Fastify plugin. Plugins are a feature of Fastify which allow us to encapsulate routes and any related functionality.

We'll start refactoring our routes module (src/routes.js) by removing some of the Express-specific lines:

-  import express from "express"

-  const router = express.Router();

-  router.use(express.json());

We then need to change the default module export to be an async function which accepts the Fastify server instance. This is the basis of a Fastify plugin. The remaining code in our routes module will be moved inside this plugin function:

export default async function routes(fastify) {
  // Configure routes
}

To make our middleware and routes work with Fastify, we need to change:

  • router references to fastify
  • route handler functions to be async
  • route handler function arguments from (request, response, next) to (request, reply)
  • response references to reply
  • calls to response.json() to reply.send()
  • instances of next(error) to throw error

After making all of these changes, our routes module is now a Fastify plugin containing Fastify routes:

// src/routes.js

import cors from "cors";

export default async function routes(fastify) {
  fastify.use(cors({ origin: true }));

  fastify.post("/", async function createUser(request, reply) {
    const newUser = request.body;

    if (!newUser) {
      throw new Error("Error creating user");
    }

    reply.status(201).send(newUser);
  });

  fastify.get("/:user_id", async function getUser(request, reply) {
    const user = {
      id: request.params.user_id,
      first_name: "Bobinsky",
      last_name: "Oso",
    };

    reply.send(user);
  });
}

We now need to change our app module (src/app.js) to use the plugin which we're exporting from the routes module. This means replacing the fastify.use() call with a call to fastify.register():

-  fastify.use("/user", routes);
+  fastify.register(routes, { prefix: "/user" });

You can view the diff of these code changes on GitHub.

Our example Express application only has one router, so we were able to migrate all of the routes in our application to use Fastify in one go. However, if we have a larger Express application with multiple routers, we could gradually migrate each router over to Fastify one at a time.

Replacing middleware with plugins

Our application is in good shape, and we've almost completely migrated it from Express to Fastify. There's one thing left to migrate: our use of the cors Express middleware package. We installed the fastify-cors plugin earlier, and now we need to add it in our application to replace the cors middleware.

In our routes module (src/routes.js), we need to replace the import of the cors middleware:

-  import cors from "cors";
+  import CorsPlugin from "fastify-cors";

We then need to replace the call to fastify.use() with a call to fastify.register():

-  fastify.use(cors({ origin: true }));
+  fastify.register(CorsPlugin, { origin: true });

Note how, when we register the plugin with Fastify, we need to pass in the plugin function and the options object as separate arguments.

As we're no longer using the use() function which the fastify-express plugin provides, we can remove it completely from our application. To do this, let's delete the following lines from our app module (src/app.js):

-  import ExpressPlugin from "fastify-express";

-  await fastify.register(ExpressPlugin);

Removing Express dependencies

The migration of our application from Express to Fastify is complete! We can now remove the Express-related dependencies by running this command in our terminal:

npm uninstall express cors fastify-express

Running our migrated application

Now that we've fully migrated our application to Fastify, it's a good time to check that everything is still working as we expect it to. Let's run the same commands which we ran earlier when our application was using Express.

First, we'll run the application in our terminal:

node src/server.js

Then, in a separate terminal, we'll make a request to the API with cURL to confirm that it's working as expected:

curl --verbose --request GET \
  --url http://localhost:3000/user/3d395cb4-531c-4989-b8ed-9cc75198187e \
  --header 'Origin: http://example-origin.com'

We should receive a response which looks like this:

< HTTP/1.1 200 OK
< vary: Origin
< access-control-allow-origin: http://example-origin.com
< content-type: application/json; charset=utf-8
< 

{"id":"3d395cb4-531c-4989-b8ed-9cc75198187e","first_name":"Bobinsky","last_name":"Oso"}

Moving Away from Middleware

Our example Express application only used a couple of middleware functions, but our real-world Express applications are likely using many more. As we've seen, the fastify-express plugin allows us to continue using Express middleware if we need to. This allows us to defer rewriting our own custom Express middleware into Fastify plugins. But what can we do about replacing third-party Express middleware?

Fortunately for us, there's a healthy ecosystem of plugins available for Fastify. Here are some of the popular Express middleware packages which we can replace with Fastify plugins:

Some of the Fastify plugins are direct ports of — or wrappers around — their Express counterparts. This means we often won't need to change the configuration options which we pass to the Fastify plugin.

You can find a comprehensive list of plugins on the Fastify Ecosystem page.

Making the Most of Fastify

Now that we've started to get comfortable with Fastify by migrating an Express application, it's a good time to start looking at other Fastify features which we can benefit from.

Validation

Fastify provides features for request validation. It uses Ajv (Another JSON schema validator) under the hood, which allows us to define validation rules with JSON Schema.

Here's an example which uses a JSON schema to validate the request body on a POST route:

const schema = {
  body: {
    type: "object",
    required: ["first_name"],
    properties: {
      first_name: { type: "string", minLength: 1 },
    },
  },
};

app.post("/user", { schema }, async (request, reply) => {
  reply.send(request.body);
});

Validation errors are automatically formatted and sent as a JSON response:

{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "body should have required property 'first_name'"
}

Learn more in the Fastify Validation and Serialization documentation.

Logging

Logging in Node.js applications can have a negative impact on performance in production. This is because there are many steps involved in serializing and transporting log data elsewhere (for example, to Elasticsearch). It's important that this aspect of our applications is highly optimized.

Logging is fully integrated in Fastify, meaning that we don't need to spend time choosing and integrating a logger. Fastify uses a fast and flexible logger: pino. It produces logs in JSON format:

{"level":30,"time":1615881822269,"pid":14323,"hostname":"localhost","msg":"Server listening at http://127.0.0.1:3000"}
{"level":30,"time":1615881829697,"pid":14323,"hostname":"localhost","reqId":"req-1","req":{"method":"GET","url":"/user/abc123","hostname":"localhost:3000","remoteAddress":"127.0.0.1","remotePort":38238},"msg":"incoming request"}
{"level":30,"time":1615881829704,"pid":14323,"hostname":"localhost","reqId":"req-1","res":{"statusCode":200},"responseTime":6.576989000663161,"msg":"request completed"}

When we create a Fastify server instance, we can enable logging and customize the options which are passed to pino. Fastify will then automatically output log messages like those shown above. The logger instance is available on the Fastify server instance (such as fastify.log.info("...")) and on all Request objects (such as request.log.info("...")).

Learn more in the Fastify Logging documentation.

Error handling

Fastify provides a setErrorHandler() method which allows us to explicitly specify a function for error handling. This is different from Express, where error handling middleware can only be distinguished by the parameters it accepts (err, req, res, next), and must be added in a specific order.

For full flexibility, we can specify different Fastify error handlers in different plugins. Learn more in the Fastify Errors documentation.

Decorators

Decorators are a powerful feature in Fastify which allow us to customize core Fastify objects — such as our Fastify server instance — and request and reply objects. Here's an example of a basic decorator:

fastify.register(async (fastify, options) => {

  fastify.decorate("yolo", () => {
    return { yo: "lo" };
  });

  fastify.get("/yolo", async function(request, reply) {
    // Our Fastify server instance is bound to `this`
    reply.send(this.yolo());
  });

});

Decorators allow us to make things like database connections or view engines available throughout our Fastify application. Learn more in the Fastify Decorators documentation.

Conclusion

In this article, we've learned how to migrate an existing Node.js application from Express to Fastify. We've seen how the fastify-express plugin can help us gradually migrate our existing applications. This allows us to start benefiting from the features which Fastify provides, even while parts of our application are still using Express.

Here are some resources you might find helpful as you move from Express to Fastify:


Originally published on SitePoint.