Managing Request-Scoped Data in Fastify

I was looking at the Fastify plugin listand found myself asking, "What is this @fastify/request-context plugin?" So, I checked it out, and here I am, writing a post about it to share what it is and how it can be useful for you! What is AsyncLocalStorage? Managing state across asynchronous operations has always been a challenge in Node.js. Traditional methods like passing context objects through function parameters or using global variables are the easiest way to share data across functions. However, they can quickly become unmanageable, especially in large applications with deeply nested asynchronous calls, making the code difficult to test. This is where AsyncLocalStorage,a core module introduced in Node.js 13, comes in handy. It provides a way to store and retrieve data that persists through an asynchronous execution context.Unlike global variables, which are shared across all requests, AsyncLocalStorage allows developersto maintain request-scoped data , meaning each incoming request gets its own isolated storage. This feature seems to overlap with Fastify's Decorators, but it's not the same. Let's see why! How It Works The basic idea behind AsyncLocalStorage is that it creates an execution context that is preservedthroughout asynchronous operations, even across setTimeout or database queries. Heres a simple example with comments: import { AsyncLocalStorage } from "async_hooks"; // Create a new instance of AsyncLocalStorage that will be unique per application const appAsyncLocalStorage = new AsyncLocalStorage(); // Simulate an incoming request every 2 seconds setInterval(() => { // Generate a random request ID that will be unique per request const requestId = Math.random().toString(36).substring(7); // Run the `reqHandler` function in the AsyncLocalStorage context // This creates the context and binds the `store` object to it const store = { requestId }; appAsyncLocalStorage.run(store, function reqHandler() { logWithRequestId("Processing request..."); setTimeout(() => logWithRequestId("Finished processing."), 3_000); }); }, 2_000); // Main business logic function // Through `appAsyncLocalStorage.getStore()`, we can access the `store` object // that was bound to the AsyncLocalStorage context in `reqHandler` function logWithRequestId(message) { const store = appAsyncLocalStorage.getStore(); const requestId = store?.requestId || "unknown"; console.log(`[Request ${requestId}]: ${message}`); } The above code snippet provides the requestId to the logWithRequestId function without passing it as a parameter! It still requires access to the appAsyncLocalStorage instance to retrieve the store object,but with a single variable, we can access everything we need throughout the request context. Why Is This Important? Without AsyncLocalStorage, you would need to manually pass the requestId to every function that requires it,which can be cumbersome and error-prone. With AsyncLocalStorage, the context is automatically preserved throughout the request lifecycle,making it much easier to track request-specific data. Think about all the times you've had to pass a logging or config object to every function. Or when you manually tracked the start and end of a request. Or even when you implemented a tracing system to follow requests through multiple services. With AsyncLocalStorage, you can forget about that spaghetti code and focus on the request context's store! How to Use the @fastify/request-context Plugin Fastify already solves multiple problems with its decorators: It provides logging through the request.log object. It provides configuration through the fastify.config object, thanks to the @fastify/env plugin. It supports Diagnostic Channels to track the request lifecycle. The @fastify/request-context plugin takes thingsfurther by offering a structured way to manage request-scoped data without the hassle of manual context management. Quick Start After installing the plugin, you can register it in your Fastify application. Lets see a real-world example: import Fastify from "fastify"; import fastifyRequestContext from "@fastify/request-context"; const app = Fastify({ logger: true }); app.register(fastifyRequestContext, { defaultStoreValues() { return { logicStep: [], }; }, }); app.get("/", async function longHandler(req, reply) { const debugBusiness = req.requestContext.get("logicStep"); // Simulate some business logic debugBusiness.push("Called external service 1"); // Do something... debugBusiness.push("Processed external service 2"); // Simulate an error throw new Error("Something went wrong

Apr 11, 2025 - 17:04
 0
Managing Request-Scoped Data in Fastify

I was looking at the Fastify plugin listand found myself asking, "What is this @fastify/request-context plugin?"

So, I checked it out, and here I am, writing a post about it to share what it is and how it can be useful for you!

What is AsyncLocalStorage?

Managing state across asynchronous operations has always been a challenge in Node.js.

Traditional methods like passing context objects through function parameters or using global variables are the easiest way to share data across functions. However, they can quickly become unmanageable, especially in large applications with deeply nested asynchronous calls, making the code difficult to test.

This is where AsyncLocalStorage,a core module introduced in Node.js 13, comes in handy.

It provides a way to store and retrieve data that persists through an asynchronous execution context.Unlike global variables, which are shared across all requests, AsyncLocalStorage allows developersto maintain request-scoped data , meaning each incoming request gets its own isolated storage.

This feature seems to overlap with Fastify's Decorators, but it's not the same. Let's see why!

How It Works

The basic idea behind AsyncLocalStorage is that it creates an execution context that is preservedthroughout asynchronous operations, even across setTimeout or database queries.

Heres a simple example with comments:

import { AsyncLocalStorage } from "async_hooks";

// Create a new instance of AsyncLocalStorage that will be unique per application
const appAsyncLocalStorage = new AsyncLocalStorage();

// Simulate an incoming request every 2 seconds
setInterval(() => {
  // Generate a random request ID that will be unique per request
  const requestId = Math.random().toString(36).substring(7);

  // Run the `reqHandler` function in the AsyncLocalStorage context
  // This creates the context and binds the `store` object to it
  const store = { requestId };
  appAsyncLocalStorage.run(store, function reqHandler() {
    logWithRequestId("Processing request...");
    setTimeout(() => logWithRequestId("Finished processing."), 3_000);
  });
}, 2_000);

// Main business logic function
// Through `appAsyncLocalStorage.getStore()`, we can access the `store` object
// that was bound to the AsyncLocalStorage context in `reqHandler`
function logWithRequestId(message) {
  const store = appAsyncLocalStorage.getStore();
  const requestId = store?.requestId || "unknown";
  console.log(`[Request ${requestId}]: ${message}`);
}

The above code snippet provides the requestId to the logWithRequestId function without passing it as a parameter!

It still requires access to the appAsyncLocalStorage instance to retrieve the store object,but with a single variable, we can access everything we need throughout the request context.

Why Is This Important?

Without AsyncLocalStorage, you would need to manually pass the requestId to every function that requires it,which can be cumbersome and error-prone.

With AsyncLocalStorage, the context is automatically preserved throughout the request lifecycle,making it much easier to track request-specific data.

Think about all the times you've had to pass a logging or config object to every function.

Or when you manually tracked the start and end of a request.

Or even when you implemented a tracing system to follow requests through multiple services.

With AsyncLocalStorage, you can forget about that spaghetti code and focus on the request context's store!

How to Use the @fastify/request-context Plugin

Fastify already solves multiple problems with its decorators:

  • It provides logging through the request.log object.
  • It provides configuration through the fastify.config object, thanks to the @fastify/env plugin.
  • It supports Diagnostic Channels to track the request lifecycle.

The @fastify/request-context plugin takes thingsfurther by offering a structured way to manage request-scoped data without the hassle of manual context management.

Quick Start

After installing the plugin, you can register it in your Fastify application.

Lets see a real-world example:

import Fastify from "fastify";
import fastifyRequestContext from "@fastify/request-context";

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

app.register(fastifyRequestContext, {
  defaultStoreValues() {
    return {
      logicStep: [],
    };
  },
});

app.get("/", async function longHandler(req, reply) {
  const debugBusiness = req.requestContext.get("logicStep");

  // Simulate some business logic
  debugBusiness.push("Called external service 1");
  // Do something...
  debugBusiness.push("Processed external service 2");

  // Simulate an error
  throw new Error("Something went wrong