Typescript

Request Context with TypeScript and Express

If you’ve ever used Express with TypeScript, you know first hand how confusing it can sometimes be. Traditional Express conventions involve the mutation of the Request object as a sort of “thread local” or “context” for the life of the request. This approach unfortunately relies on the loosely typed nature and conventions surrounding vanilla JavaScript, and does not scale well in a TypeScript project.

When building APIs with Express + Javascript, you’ll usually see something like this via Express middleware to decorate values and objects to the req object for later middleware / route controllers to consume:

// ...

app.use((req, res, next) => {
  req.foo = 'hello world';
  next();
});

With TypeScript, this has an obvious pitfall: the request object is strongly typed in most projects, so you now have to either create a custom request type or extend the default request type for every property you plan on decorating the request object with. I’ve found a much better approach is avoid decorating the request object all together, and instead rely on middleware to bind a Request Context object that can contain all metadata you wish to associate with a given Request.

To do this is dead simple using WeakMaps. The reason we want to use WeakMaps for something like this is that we want the Request Context object to be garbage collected as soon as the Request itself is garbage collected, to avoid memory leaks. A simple implementation involves a piece of middleware along with a Context class that is strongly typed.

To begin with, let’s implement a simple class that weakly bind itself to a number of incoming Request objects:

import { Request } from 'express';

export default class Context {
  static _bindings = new WeakMap<Request, Context>();
  
  public foo = 'bar';
   
  constructor () {}
    
  static bind (req: Request) : void {
    const ctx = new Context();
    Context._bindings.set(req, ctx);
  }
    
  static get (req: Request) : Context | null {
    return Context._bindings.get(req) || null;
  }
}

Next, we need a simple piece of Middleware to bind each Context to the Request object:

import express, { Request } from 'express';
import Context from './context'

const app = express();

// Middleware
app.use((req: Request, res: any, next: any) => {
  Context.bind(req);
  next();
});

// ...

Finally, we can access our Context within route controllers:

import express, { Request } from 'express';
import Context from './context'

const app = express();

// ...

app.get('/foo', (req: Request, res) => {
  const ctx: Context = Context.get(req);
  
  // Here we can use our Context!

  res.status(200).send(ctx.foo);
});

...and there you have it! You can now “attach” objects to the Request without actually decorating it and requiring annoying type annotation additions, all with the JavaScript primitive WeakMap.

Have questions about our content?

Join our developer community and ask a BoltSource Engineer today