Request Context Management in NestJS: Replacing express-http-context

Request Context Management in NestJS: Replacing express-http-context

Creating a Generic Request-Scoped Context Module in NestJS

Featured on Hashnode

Introduction

When building web applications, you often need to store and manage request-specific data throughout your application. In Express-based applications, developers commonly use the express-http-context package to store and retrieve data related to the current request.

express-http-context provides a set of simple functions to set and get request-specific data:

const httpContext = require('express-http-context');

// Store data
httpContext.set('key', value);

// Retrieve data
const value = httpContext.get('key');

This package is widely used for various purposes, such as:

  1. Storing user authentication tokens or user information.

  2. Managing API request identifiers for logging and debugging.

  3. Handling custom data that needs to be available across middleware, services, or other parts of the application.

However, in NestJS, there is no built-in module specifically designed for this purpose. While you can still use express-http-context together with NestJS, it's better to take advantage of the framework's built-in features for managing request-specific data.

Why Replace express-http-context?

There are several reasons to consider replacing express-http-context with a NestJS-based solution:

  1. Package Inactivity: The express-http-context package has not been actively maintained since 2020, which may cause compatibility issues with newer versions of Node.js or other dependencies.

  2. Middleware Order Dependency: When using express-http-context, it is crucial to use the middleware immediately before the first middleware that needs access to the context. If not done correctly, developers may not have access to the context in any middleware "used" before this one. This can lead to subtle bugs and confusion, especially for developers who might overlook this critical detail.

  3. NestJS Integration: NestJS provides built-in features like dependency injection and request-scoped services that allow for a more idiomatic and streamlined approach to managing request-specific data.

  4. Flexibility: By creating a custom request-scoped module, you have full control over the implementation, allowing you to tailor it to your application's specific needs.

In this blog post, we will demonstrate how to create a generic request-scoped context module in NestJS that you can use to manage request context throughout your application, similar to how express-http-context works.

Implementation

Step 1: Create the RequestContextService

First, we need to create a service that will be responsible for storing and retrieving request-specific data. We will name this service RequestContextService. This service should be request-scoped to ensure that a new instance is created for each incoming request.

Here's the code for the RequestContextService:

// src/request-context/request-context.service.ts

@Injectable({ scope: Scope.REQUEST })
export class RequestContextService {
  private readonly contextMap: Map<string, any> = new Map();

  constructor(@Inject(REQUEST) private readonly request: Request) {}

  set<T>(key: string, value: T): void {
    this.contextMap.set(key, value);
  }

  get<T>(key: string): T | undefined {
    return this.contextMap.get(key) as T;
  }

  getRequest(): Request {
    return this.request;
  }
}

In this service, we maintain a private contextMap to store the request-specific data. We also provide set and get generic methods, allowing you to specify the type of value being stored or retrieved.

The getRequest method returns the current Request object, allowing you to access the request-specific data in other parts of your application.

Step 2: Create the RequestContextModule

Next, we need to create a module to export the RequestContextService. This module will allow us to import the service into other modules and make it available for injection throughout the application.

Here's the code for the RequestContextModule:

// src/request-context/request-context.module.ts

@Module({
  providers: [RequestContextService],
  exports: [RequestContextService],
})
export class RequestContextModule {}

This module is a simple NestJS module that defines the RequestContextService as a provider and exports it.

Step 3: Using RequestContextService in a Guard

In this example, we will use the RequestContextService in a custom UserGuard to store the user's authentication token.

Here's the code for the UserGuard:

// src/users/user.guard.ts

@Injectable()
export class UserGuard implements CanActivate {
  constructor(private requestContextService: RequestContextService) {}

  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    // Extract the user token from the request headers
    const userToken: string = request.headers['x-access-token'];
    // Store the user token in RequestContextService
    this.requestContextService.set<string>('userToken', userToken);
    // Add your authentication logic here
    const isAuthenticated = this.authenticate(userToken);

    return isAuthenticated;
  }

  private authenticate(userToken: string): boolean {
    // Implement your token validation logic here
    return true; // Assume the user is authenticated for demo purposes
  }
}

In this guard, we inject the RequestContextService and use the set method to store the user's authentication token with a key of 'userToken'. You can implement your own authentication logic in the authenticate method.

Step 4: Using RequestContextService in a Service

Now, let's demonstrate how to use the RequestContextService in a service to retrieve back the user's authentication token and perform actions based on it.

Here's the code for the UsersService:

// src/users/users.service.ts

@Injectable()
export class UsersService {
  constructor(private requestContextService: RequestContextService) {}

  getUserToken(): string {
    return this.requestContextService.get<string>('userToken');
  }
}

In this service, we inject the RequestContextService and use the get method to retrieve the user's authentication token using the key 'userToken'. You can then use the token to perform actions based on the user's identity or permissions, or implement other methods that require request-specific data.

To make use of our UserGuard and UsersService above, we can have a controller with an endpoint protected by the guard and triggers the service for token retrieval.

// src/users/users.controller.ts

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get('token')
  // Protect and authenticate the endpoint with UserGuard
  @UseGuards(UserGuard)
  @ApiHeader({
    name: 'x-access-token',
    required: true,
  })
  getToken(): string {
    return this.usersService.getUserToken();
  }
}

Now, we should have a working demo of a request-scoped context module. You may try spinning up the service on your local machine.

After starting the service on your localhost, you may try calling the endpoint with the following cURL command to verify it. Replace the {YOUR_ACCESS_TOKEN} with your own value.

curl -X 'GET' \
  'http://localhost:3000/users/token' \
  -H 'accept: application/json' \
  -H 'x-access-token: {YOUR_ACCESS_TOKEN}'

// Response
{YOUR_ACCESS_TOKEN}

If you are getting a response with your {YOUR_ACCESS_TOKEN} value relayed back to you, congratulation! Your service is working as expected.

Conclusion

In this blog post, we demonstrated how to create a generic request-scoped context module in NestJS that allows you to manage request context throughout your application, similar to how express-http-context works. By taking advantage of NestJS's built-in features like dependency injection and request-scoped services, you can implement a custom solution that fits your requirements.

This approach enables you to store and access request-specific data across different parts of your application, making it easier to manage data related to the current request. Now you can enjoy a more streamlined way of handling request context in your NestJS applications.

Here's the full source code for reference:


Caution

Setting RequestContextService to request-scoped can have some performance implications during high traffic load, but the impact should be minimal if implemented correctly.

Request-scoped services in NestJS are instantiated once per request, meaning that a new instance of the service is created for each incoming request. While this can lead to increased memory usage and slightly higher instantiation times, it is generally not a significant performance concern for most applications.

However, do keep the following considerations in mind to minimize the impact on performance:

  1. Minimize dependencies: Ensure that your RequestContextService has minimal dependencies on other services or components. The more dependencies a service has, the more complex and time-consuming its instantiation process will be.

  2. Optimize data storage: Use efficient data structures for storing request-specific data. In the example provided, we used a Map to store the data. Maps offer good performance for most use cases, but you may need to consider other data structures depending on your specific needs.

  3. Avoid expensive operations: Keep the logic within RequestContextService as simple as possible. Avoid performing expensive operations or calculations within the service, as this will increase the processing time for each request.

  4. Monitor performance: Regularly monitor your application's performance, especially under high traffic loads, to identify potential bottlenecks and address them as needed.

By following best practices and keeping these considerations in mind, the performance impact of using request-scoped services like RequestContextService should be minimal. In many cases, the benefits of using request-scoped services, such as improved code organization, security, and easier management of request-specific data, far outweigh the minor performance trade-offs.