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

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