Categories
Fastify Node.js

Use Fastify hooks to set headers on every response

4 min read

Often we’ll find we need to set the same response headers for multiple routes in an API, but we don’t want to duplicate the setting of those headers in every route handler. Hooks are a core feature of Fastify that we can use to help us solve this problem.

Let’s take a look at what Fastify hooks are and how we can use them to set the same response headers for multiple routes.

All the code examples in this article are also on GitHub in my Fastify examples repository.

Jump links

Fastify hooks and encapsulation contexts

A hook in Fastify is an event handler function that has been registered against a specific event. Most of the events triggered by Fastify relate to the different stages of the request/response lifecycle. There are also a few events that are triggered at the application level. They’re explained in-depth in the Fastify Hooks documentation.

When we create a Fastify server instance, there’s a root encapsulation context that we can register hooks, decorators, routes, plugins and an error handler in. Plugins have their own encapsulation context too, allowing us to register any of those same entities inside of them. These different encapsulation contexts allow us to have fine-grained control over what code we run and when.

In order to set headers for multiple routes we want to register an onSend hook.

Example of an onSend hook

Here’s an example of registering an onSend hook:

/**
 * Assumes a Fastify server instance exists named `app`.
 */
app.addHook("onSend", async function (request, reply) {
	reply.headers({
		"Cache-Control": "no-store",
		Pragma: "no-cache",
	});
});

View code example on GitHub

In the example above we’re sending cache response headers to control how clients cache the responses from our API. Now let’s take a look at the different places we might want to register this onSend hook.

Set headers for a collection of routes

Here is a plugin that encapsulates a route and an onSend hook:

// routes.js

export default async function routesPlugin(app) {
	app.get("/results-b", async function (request, reply) {
		return [
			{ score: 51, date: "2022-07-12" },
			{ score: 32, date: "2022-07-11" },
		];
	});

	/**
	 * Registering an `onSend` hook in a plugin encapsulation context.
	 */
	app.addHook("onSend", async function (request, reply) {
		reply.headers({
			"Cache-Control": "no-store",
			Pragma: "no-cache",
		});
	});
}

The onSend hook that is registered in the plugin above will only trigger for routes that are registered inside that plugin or in a child plugin.

In this next code block we’re registering the routes plugin in our Fastify server’s root encapsulation context, and then registering another route in that root context:

// server.js
import Fastify from "fastify";

import routes from "./routes.js";

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

/**
 * A plugin that registers an `onSend` hook and a route.
 */
app.register(routes);

/**
 * The `onSend` hook will *not* run for this route as it
 * hasn't been registered in this encapsulation context.
 */
app.get("/results-a", async function (request, reply) {
	return [
		{ score: 51, date: "2022-07-12" },
		{ score: 32, date: "2022-07-11" },
	];
});

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

The onSend hook is only triggered for the /results-b route, meaning that the cache headers are sent for this route, but not for the /results-a route. Here are the HTTP responses from these routes:

GET /results-a

HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
content-length: 67
Date: Wed, 20 Jul 2022 14:12:17 GMT
Connection: keep-alive
Keep-Alive: timeout=72

GET /results-b

HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
cache-control: no-store
pragma: no-cache
content-length: 67
Date: Wed, 20 Jul 2022 14:12:17 GMT
Connection: keep-alive
Keep-Alive: timeout=72

One important thing to note is that the ordering of routes and hooks don’t matter — the encapsulation context they’re registered in determines what code will run and when.

Set headers for all server routes

Approach 1: Register an onSend hook in the server’s root context

// server.js
import Fastify from "fastify";

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

/**
 * Registering an `onSend` hook in the root encapsulation context.
 */
app.addHook("onSend", async function (request, reply) {
	reply.headers({
		"Cache-Control": "no-store",
		Pragma: "no-cache",
	});
});

/**
 * The `onSend` hook will run for this route, and any others
 * that are added to the root context, or a child context.
 */
app.get("/results", async function (request, reply) {
	return [
		{ score: 51, date: "2022-07-12" },
		{ score: 32, date: "2022-07-11" },
	];
});

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

View code example on GitHub

In the example above, the onSend hook is registered in the server’s root context. This means it will run for all routes that are registered, regardless of whether they are encapsulated inside plugins.

When a plugin is registered, a new encapsulation context is created (a child context) that inherits the context that it was registered in (the parent context). This means that any hooks, decorators or error handlers that were registered in the parent context also exist in the child context.

Approach 2: Register an onSend hook inside a plugin and wrap it with fastify-plugin

If we want to abstract our onSend hook into a separate module, but still have it trigger for all server routes, we need to wrap it using fastify-plugin:

// cache-headers-plugin.js
import fastifyPlugin from "fastify-plugin";

async function cacheHeadersPlugin(app) {
	/**
	 * Registering an `onSend` hook in a plugin encapsulation context.
	 */
	app.addHook("onSend", async function (request, reply) {
		reply.headers({
			"Cache-Control": "no-store",
			Pragma: "no-cache",
		});
	});
}

/**
 * Share the context of this plugin with the context that registers it.
 */
export default fastifyPlugin(cacheHeadersPlugin);

Wrapping the plugin with fastify-plugin "breaks" the plugin’s encapsulation context. This means that when we register that plugin, anything registered inside it is exposed to the registration context.

Now by registering the plugin in the server’s root context, the onSend hook in the cache headers plugin will be triggered for all server routes:

// server.js
import Fastify from "fastify";

import cacheHeadersPlugin from "./cache-headers-plugin.js";

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

/**
 * A plugin that registers an `onSend` hook.
 *
 * Everything registered in that plugin is shared with this
 * context by wrapping it with `fastify-plugin`.
 */
app.register(cacheHeadersPlugin);

/**
 * The `onSend` hook will run for this route, and any others
 * that are added to the root context, or a child context.
 */
app.get("/results", async function (request, reply) {
	return [
		{ score: 51, date: "2022-07-12" },
		{ score: 32, date: "2022-07-11" },
	];
});

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