DEV Community

Cover image for Getting Started with Fastify for Node.js
Damilola Olatunji for AppSignal

Posted on • Originally published at blog.appsignal.com

Getting Started with Fastify for Node.js

Chances are high that you've previously worked with Express, as it's been the go-to web framework for Node.js developers since its release in 2010. However, in recent years, newer web frameworks have emerged, and Express's development has slowed down significantly.

Fastify is a relatively new player on the scene, but it's quickly gaining popularity due to its speed and unique features.

If you're still using Express, you might wonder if it's worth switching to Fastify. We've created this three-part series to explore the benefits of switching from Express to Fastify, as well as the potential challenges you might encounter. It'll also provide a hands-on guide for migrating your existing Express applications to Fastify.

In this part, we'll focus on why you might consider Fastify over Express for your next Node.js project and explore some of Fastify's core concepts in detail.

So let's dive in and see what Fastify has to offer!

Prerequisites

This tutorial assumes that you have a recent version of Node.js installed on your computer, such as the latest LTS release (v18.x at the time of writing). Its code samples use top-level await and the ES module syntax instead of CommonJS. We also assume you have a basic knowledge of building Node.js-backed APIs using Express or other web frameworks.

Why Switch from Express to Fastify for Node.js?

Before delving into the inner workings of Fastify, it is important to understand why you may want to consider using it for your next Node.js project. This section covers a few of the benefits that Fastify offers when compared to Express, at the time of writing:

1. Faster Performance

Fastify was designed to be fast from the ground up and use fewer system resources, so it handles more requests per second compared to Express and other Node.js web frameworks.

Although Express is a more mature framework with a larger community and a more comprehensive ecosystem of third-party packages, it is not as optimized for performance as Fastify.

2. Plugin Architecture

Fastify boasts a powerful plugin system that can easily add custom functionality to your application. Many official plugins handle things like authentication, validation, security, database connections, serverless compatibility, and rate-limiting. There's also a growing ecosystem of third-party plugins that can be integrated into your application, and you can easily create your own plugins.

Express is extensible through middleware functions injected into the request/response processing pipeline to modify an application's behavior. Still, they are tightly coupled into the framework and less flexible or modular than Fastify's approach. Plugins in Fastify are also encapsulated by default, so you don't experience issues caused by cross-dependencies.

3. Safer by Default

Security is a critical consideration when building web applications, and Fastify provides a number of built-in security features, such as:

  • automatically escaping output
  • input validation
  • preventing content sniffing attacks
  • protecting against malicious header injection

Its plugin system also makes it easy to apply additional security measures to your application.

On the other hand, Express relies more heavily on middleware to handle security concerns. It does not have built-in validation or schema-based request handling, although several third-party packages can be utilized for this purpose.

4. Modern JavaScript Features

Fastify has built-in support for modern JavaScript features, such as async/await and Promises. It also automatically catches uncaught rejected promises that occur in route handlers, making it easier to write safe asynchronous code.

Express doesn't support async/await handlers, though you can add these with a package like express-async-errors. Note that this feature will be supported natively in Express 5 when it comes out of beta.

app.get("/", async function (request, reply) {
  var data = await getData();
  var processed = await processData(data);
  return processed;
});
Enter fullscreen mode Exit fullscreen mode

5. Built-in JSON Schema Validation

In Fastify, JSON schema validation is a built-in feature that allows you to validate the payload of incoming requests before the handler function is executed. This ensures that incoming data is in the expected format and meets the required criteria for your business logic. Fastify's JSON schema validation is powered by the Ajv library, a fast and efficient JSON schema validator.

const bodySchema = {
  type: "object",
  properties: {
    first_name: { type: "string" },
    last_name: { type: "string" },
    email: { type: "string", format: "email" },
    phone: { type: "number" },
  },
  required: ["first_name", "last_name", "email"],
};

fastify.post(
  "/create-user",
  {
    schema: {
      body: bodySchema,
    },
  },
  async (request, reply) => {
    const { first_name, last_name, age, email } = request.body;
    reply.send({ first_name, last_name, age, email });
  }
);
Enter fullscreen mode Exit fullscreen mode

In contrast, Express does not provide built-in support for JSON schema validation. However, you can use third-party libraries like Joi or the aforementioned Ajv package to validate JSON payloads in Express applications (this requires additional setup and configuration).

6. TypeScript Support

Fastify has excellent TypeScript support and is built with TypeScript in mind. Its type definitions are included in the package, and it supports using TypeScript to define types for route handlers, schemas, and plugins.

From v3.x, the Fastify type system heavily relies on generic properties for accurate type checking.

Express also supports TypeScript (through the @types/express package), but its support is less comprehensive than Fastify.

7. A Built-in Logger

Fastify provides a built-in logging mechanism based on Pino that allows you to capture various events in your applications. Once enabled, Fastify logs all incoming requests to the server and errors that occur while processing said requests. It also provides a convenient way to log custom messages through the log() method on the Fastify instance or the request object.

const app = require("fastify")({
  logger: true,
});

app.get("/", function (request, reply) {
  request.log.info("something happened");
  reply.send("Hello, world!");
});
Enter fullscreen mode Exit fullscreen mode

Express does not provide a built-in logger. Instead, you need to use third-party logging libraries like Morgan, Pino, or Winston to log HTTP requests and responses. While these libraries are highly configurable, they require additional setup and configuration.

Fastify Offers More than Express

As you can see, Fastify offers numerous advantages over Express, making it a compelling option for building Node.js web applications. In the following sections, we will dive deeper into Fastify's core features and demonstrate how to create web servers and routes.

We will also explore how Fastify's extensibility and plugin system allows for greater flexibility in application development.

By the end, you will better understand why Fastify is a great option for building high-performance and scalable Node.js applications.

Getting Started with Fastify for Your Node.js Application

Before you can utilize Fastify in your project, you need to install it first:

npm install fastify
Enter fullscreen mode Exit fullscreen mode

Once it's installed, you can import it into your project and instantiate a new Fastify server instance, as shown below:

import Fastify from "fastify";

const fastify = Fastify();
Enter fullscreen mode Exit fullscreen mode

The Fastify function accepts an options object that customizes the server's behavior. For example, you can enable its built-in logging feature and specify a timeout value for incoming client requests through the snippet below:

. . .
const fastify = Fastify({
  logger: true,
  requestTimeout: 30000, // 30 seconds
});
Enter fullscreen mode Exit fullscreen mode

After configuring your preferred options, you can add your first route as follows:

. . .
fastify.get('/', function (request, reply) {
  reply.send("Hello world!")
})
Enter fullscreen mode Exit fullscreen mode

This route accepts GET requests made to the server root and responds with "Hello world!". You can then proceed to start the server by listening on your preferred localhost port:

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

Start the server by executing the entry file. You will observe some JSON log output in the console:

{"level":30,"time":1675958171939,"pid":391638,"hostname":"fedora","msg":"Server listening at http://127.0.0.1:3000"}
{"level":30,"time":1675958171940,"pid":391638,"hostname":"fedora","msg":"Server listening at http://[::1]:3000"}
{"level":30,"time":1675958171940,"pid":391638,"hostname":"fedora","msg":"Fastify is listening on port: http://127.0.0.1:3000"}
Enter fullscreen mode Exit fullscreen mode

You can use the pino-pretty package to make your application logs easier to read in development.

After installing the package, pipe your program's output to the CLI as follows:

node server.js | pino-pretty
Enter fullscreen mode Exit fullscreen mode

You'll get a colored output that looks like this:

pino-pretty in action

Enabling the logger is particularly helpful as it logs all incoming requests to the server in the following manner:

curl "http://localhost:3000"
Enter fullscreen mode Exit fullscreen mode
{
  "level": 30,
  "time": 1675961032671,
  "pid": 450514,
  "hostname": "fedora",
  "reqId": "req-1",
  "res": { "statusCode": 200 },
  "responseTime": 3.1204520016908646,
  "msg": "request completed"
}
Enter fullscreen mode Exit fullscreen mode

Notice how you get a timestamp, request ID, response status code, and response time (in milliseconds) in the log. This is a step up from Express where you get no such functionality until you integrate Pino (or other logging frameworks) yourself.

Creating Routes in Fastify

Creating endpoints in Fastify is easy using several helper methods in the framework. The fastify.get() method (seen in the previous section) creates endpoints that accept HTTP GET requests. Similar methods also exist for HEAD, POST, PUT, DELETE, PATCH, and OPTIONS requests:

fastify.get(path, [options], handler);
fastify.head(path, [options], handler);
fastify.post(path, [options], handler);
fastify.put(path, [options], handler);
fastify.delete(path, [options], handler);
fastify.options(path, [options], handler);
fastify.patch(path, [options], handler);
Enter fullscreen mode Exit fullscreen mode

If you'd like to use the same handler for all supported HTTP request methods on a specific route, you can use the fastify.all() method:

fastify.all(path, [options], handler);
Enter fullscreen mode Exit fullscreen mode

As you can see from the above signatures, the options argument is optional, but you can use it to specify a myriad of configuration settings per route. See the Fastify docs for the full list of available options.

The path argument can be static (like /about or /settings/profile) or dynamic (like /article/:id, /foo*, or /:userID/repos/:projectID). URL parameters in dynamic URLs are accessible in the handler function through the request.params object:

fastify.get("/:userID/repos/:projectID", function (request, reply) {
  const { userID, projectID } = request.params;
  // rest of your code``
});
Enter fullscreen mode Exit fullscreen mode

Fastify handlers have the following signature, where request represents the HTTP request received by the server, and reply represents the response from an HTTP request.

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

If the handler function for a route is asynchronous, you can send a response by returning from the function:

fastify.get("/", async function (request, reply) {
  // do some work
  return { body: true };
});
Enter fullscreen mode Exit fullscreen mode

If you're using reply in an async handler, await reply or return reply to avoid race conditions:

fastify.get("/", async function (request, reply) {
  // do some work
  return reply.send({ body: true });
});
Enter fullscreen mode Exit fullscreen mode

One neat aspect of Fastify routes is that they automatically catch uncaught exceptions or promise rejections. When such exceptions occur, the default error handler is executed to provide a generic '500 Internal Server Error' response.

fastify.get("/", function (request, reply) {
  throw new Error("Uncaught exception");
});
Enter fullscreen mode Exit fullscreen mode

Here's the JSON response you get when you use curl to send a request to the endpoint above:

{"statusCode":500,"error":"Internal Server Error","message":"Uncaught exception"}
Enter fullscreen mode Exit fullscreen mode

You can modify the default error-handling behavior through the
fastify.setErrorHandler() function.

Plugins in Fastify

Fastify is designed to be extensible through plugins. Plugins are essentially self-contained, encapsulated, and reusable modules that add custom logic and behavior to a Fastify server instance. They can be used for a variety of purposes, such as integrating with a protocol, framework, database, or an API, handling authentication, and much more.

At the time of writing, there are over 250 plugins available for Fastify. Some are maintained by the core team, but the community provides most of them. When you find a plugin you'd like to use, you must install it first, then register it on the Fastify instance.

For example, let's use the @fastify/formbody plugin to add support for x-www-form-urlencoded bodies to Fastify:

npm install @fasitfy/formbody
Enter fullscreen mode Exit fullscreen mode
// import the plugin
import formBody from '@fastify/formbody';

. . .

// register it on your Fastify instance
await fastify.register(formBody);

fastify.post('/form', function (request, reply) {
  reply.send(request.body);
});

. . .
Enter fullscreen mode Exit fullscreen mode

With this plugin installed and registered, you'll be able to access x-www-form-urlencoded form bodies as an object. For example, the following request:

curl -d "param1=value1&param2=value2" -X POST 'http://localhost:3000/form'
Enter fullscreen mode Exit fullscreen mode

Should produce the output below:

{"param1":"value1","param2":"value2"}
Enter fullscreen mode Exit fullscreen mode

You can also create a custom Fastify plugin quite easily. All you need to do is export a function that has the following signature:

function (fastify, opts, next) {}
Enter fullscreen mode Exit fullscreen mode

The first parameter is the Fastify instance that the plugin is being registered to, the second is an options object, and the third is a callback function that must be called when the plugin is ready.

Below is an example of a simple Fastify plugin that adds a new health check route to the server:

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

  done();
}

export default health;
Enter fullscreen mode Exit fullscreen mode
// server.js
import health from "./plugin.js";

const fastify = Fastify({ logger: true });
await fastify.register(health);
Enter fullscreen mode Exit fullscreen mode

At this point, requests to http://localhost:3000/health will yield the following response:

{ "status": "up" }
Enter fullscreen mode Exit fullscreen mode

How Plugins in Fastify Work

Plugins in Fastify create a new encapsulation context isolated from all other contexts in the application by default. This allows you to modify the fastify instance within the plugin's context without affecting the state of any other contexts. There is always only one root context in a Fastify application, but you can have as many child contexts as you want.

Here's a code snippet illustrating how contexts work in Fastify:

// `root` is the root context.
const root = Fastify({
  logger: true,
});

root.register(function pluginA(contextA, opts, done) {
  // `contextA` is a child of the `root` context.
  contextA.register(function pluginB(contextB, opts, done) {
    // `contextB` is a child of `contextA`
    done();
  });

  done();
});

root.register(function pluginC(contextC, opts, done) {
  // `contextC` is a child of the `root` context.
  contextC.register(function pluginD(contextD, opts, done) {
    // `contextD` is a child of `contextC`
    done();
  });

  done();
});
Enter fullscreen mode Exit fullscreen mode

The snippet above describes a Fastify application with five different encapsulation contexts. root is the parent of two contexts (contextA and contextC), and each one has its own child context (contextB and contextD, respectively).

Every context (or plugin) in Fastify has its own state, which includes decorators, hooks, routes, or plugins. While child contexts can access the state of the parent, the reverse is not true (at least by default).

Here's an example that uses decorators (we'll further discuss decorators in part two of this series) to attach some custom properties to each context:

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

root.decorate("answerToLifeTheUniverseAndEverything", 42);

await root.register(async function pluginA(contextA, opts, done) {
  contextA.decorate("speedOfLight", "299,792,458 m/s");

  console.log(
    "contextA -> root =",
    contextA.answerToLifeTheUniverseAndEverything
  );
  await contextA.register(function pluginB(contextB, opts, done) {
    contextB.decorate("someAPIKey", "3493203890");

    console.log(
      "contextB -> root =",
      contextB.answerToLifeTheUniverseAndEverything
    );
    console.log("contextB -> contextA =", contextB.speedOfLight);
    done();
  });

  console.log("contextA -> contextB =", contextA.someAPIKey);

  done();
});

console.log("root -> contextA =", root.speedOfLight);
console.log("root -> contextB =", root.someAPIKey);
Enter fullscreen mode Exit fullscreen mode

In the snippet above, the root context is decorated with the custom property answerToLifeTheUniverseAndEverything, which yields 42. Similarly, contextA and contextB are decorated with their own custom properties. When you execute the code, you will observe the following results in the console:

contextA -> root = 42
contextB -> root = 42
contextB -> contextA = 299,792,458 m/s
contextA -> contextB = undefined
root -> contextA = undefined
root -> contextB = undefined
Enter fullscreen mode Exit fullscreen mode

Notice that the root's custom property is accessible directly on the contextA and contextB objects since they are both descendants of the root context. Similarly, contextB can access the speedOfLight property added to contextA for the same reason.

However, since parents cannot access the state of their nested contexts, accessing contextA.someAPIKey, root.speedOfLight, and root.someAPIKey produces undefined.

Sharing Context in Fastify for Node.js

There is a way to break Fastify's encapsulation mechanism so that parents can also access the state of their child contexts. This is done by wrapping the plugin function with the fastify-plugin module:

// import the plugin
import fp from "fastify-plugin";

await root.register(
  // wrap the plugin function
  fp(async function pluginA(contextA, opts, done) {
    // . . .
  })
);
Enter fullscreen mode Exit fullscreen mode

With this in place, you'll observe the following output:

contextA -> root = 42
contextB -> root = 42
contextB -> contextA = 299,792,458 m/s
contextA -> contextB = undefined
root -> contextA = 299,792,458 m/s
root -> contextB = undefined
Enter fullscreen mode Exit fullscreen mode

Notice that the speedOfLight property decorated on contextA is now accessible in the root context. However, someAPIKey remains inaccessible because the pluginB function isn't wrapped with the fastify-plugin module.

Here's the solution if you also intend to access contextB's state in a parent context:

// import the plugin
import fp from "fastify-plugin";

await root.register(
  // wrap the plugin function
  fp(async function pluginA(contextA, opts, done) {
    // . . .

    await contextA.register(
      fp(function pluginB(contextB, opts, done) {
        // . . .
      })
    );
  })
);
Enter fullscreen mode Exit fullscreen mode

contextB's state is now also accessible in all of its ancestors:

contextA -> root = 42
contextB -> root = 42
contextB -> contextA = 299,792,458 m/s
contextA -> contextB = 3493203890
root -> contextA = 299,792,458 m/s
root -> contextB = 3493203890
Enter fullscreen mode Exit fullscreen mode

You'll see a more practical example of the plugin encapsulation context in the next part of this series, where we'll tackle hooks and middleware.

Coming Up Next: Hooks, Middleware, Decorators, and Validation

In this article, we introduced the Fastify web framework, exploring its convenience, speed, and low overhead, which makes it a popular choice for building highly performant and scalable web applications. We compared Fastify to Express and highlighted why you might want to consider switching.

We also discussed Fastify's extensibility and plugin system, which allow you to customize and extend its functionality.

In part two of this series, we will dive deeper into some of the more advanced concepts of Fastify, such as hooks, middleware, decorators, and validation.

Until next time, thanks for reading!

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)