javascript

Getting Started with Fastify for Node.js

Damilola Olatunji

Damilola Olatunji on

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.

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

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.

javascript
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 }); } );

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.

javascript
const app = require("fastify")({ logger: true, }); app.get("/", function (request, reply) { request.log.info("something happened"); reply.send("Hello, world!"); });

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:

shell
npm install fastify

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

javascript
import Fastify from "fastify"; const fastify = Fastify();

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:

javascript
. . . const fastify = Fastify({ logger: true, requestTimeout: 30000, // 30 seconds });

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

javascript
. . . fastify.get('/', function (request, reply) { reply.send("Hello world!") })

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:

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

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

json
{"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"}

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:

shell
node server.js | pino-pretty

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:

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

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:

javascript
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);

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:

javascript
fastify.all(path, [options], handler);

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:

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

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.

javascript
function (request, reply) {}

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

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

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

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

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.

javascript
fastify.get("/", function (request, reply) { throw new Error("Uncaught exception"); });

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

json
{"statusCode":500,"error":"Internal Server Error","message":"Uncaught exception"}⏎

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:

shell
npm install @fasitfy/formbody
javascript
// 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); }); . . .

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:

shell
curl -d "param1=value1&param2=value2" -X POST 'http://localhost:3000/form'

Should produce the output below:

json
{"param1":"value1","param2":"value2"}⏎

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

javascript
function (fastify, opts, next) {}

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:

javascript
// plugin.js function health(fastify, options, done) { fastify.get("/health", (request, reply) => { reply.send({ status: "up" }); }); done(); } export default health;
javascript
// server.js import health from "./plugin.js"; const fastify = Fastify({ logger: true }); await fastify.register(health);

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

json
{ "status": "up" }

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:

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

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:

javascript
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);

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:

text
contextA -> root = 42 contextB -> root = 42 contextB -> contextA = 299,792,458 m/s contextA -> contextB = undefined root -> contextA = undefined root -> contextB = undefined

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:

javascript
// import the plugin import fp from "fastify-plugin"; await root.register( // wrap the plugin function fp(async function pluginA(contextA, opts, done) { // . . . }) );

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

text
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

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:

javascript
// 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) { // . . . }) ); }) );

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

text
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

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.

Damilola Olatunji

Damilola Olatunji

Damilola is a freelance technical writer and software developer based in Lagos, Nigeria. He specializes in JavaScript and Node.js, and aims to deliver concise and practical articles for developers. When not writing or coding, he enjoys reading, playing games, and traveling.

All articles by Damilola Olatunji

Become our next author!

Find out more

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps