At Cerebrum we’re big fans of Prisma, which makes it super easy to create and migrate data models. However, one difficulty we’ve found is that the middleware functionality offered by Prisma is essentially stateless and doesn’t give you an out-of-the-box way to access the context in which the Prisma operation is running. This is a real headache for features such as advanced logging, or applying permissions, where the context is required to access things like the current user, or request headers.

To fix this issue, we’ve used the npm module cls-hooked, which allows us to store a variable isolated inside the current call stack. The isolation is essential because, without the isolation, we may end up with the wrong context data for a request when our API server is serving multiple requests at once.

For this example, we will strip back the code to something very basic, though the principle here still applies if you’re using Prisma with express, apollo or something similar as your API framework. The following code comes from the Prisma getting started guide:

async function main() {
        const user = await prisma.user.create({
        data: {
          name: "Alice",
          email: "alice@prisma.io",
        },
      });
      console.log(user);
}

Supposing we wanted to log a contextual value using middleware here, the first thing we need to do is create a namespace with cls-hooked. The namespace label can be anything you want, but try to keep it intuitive.

const clsSession = createNamespace("server");

We then wrap our Prisma call inside clsSession.run , this will allow us to access set and get variables inside the call context. Not that the .run call has been wrapped in a Promise, so we can still await the returned user object.

async function main() {
  const user = await new Promise((resolve) => {
    clsSession.run(async () => {
      const response = await prisma.user.create({
        data: {
          name: "Alice",
          email: "alice@prisma.io",
        },
      });
      resolve(response);
    });
  });

  console.log(user);
}

We can now set values to this namespace, and they will be accessible to any code running inside this call stack by using clsSession.get.

  async function main() {
      const user = await new Promise((resolve) => {
        clsSession.run(async () => {
          clsSession.set("ctx", {
            userId: "123",
          });

          const response = await prisma.user.create({
            data: {
              name: "Alice",
              email: "alice@prisma.io",
            },
          });
          resolve(response);
        });
      });

      console.log(user);
  }

Now we can set up a Prisma middleware and use the clsSession to retrieve the contextual data:

const middleware: Prisma.Middleware = async (params, next) => {
  const ctx = clsSession.get("ctx");
  console.log(
    `user ${ctx.userId} is running action ${params.action} on model ${params.model}`
  ); // e.g. `user 123 is running action create on model User`
  return next(params);
};

prisma.$use(middleware);

Async function main() {
 …
}

Running this code will cause the log message `user 123 is running action create on model User` to be printed to the console. Although the example shown here is trivial, the same principle can be applied to more complex setups, e.g. in express, you would call `clsSession.run` inside a middleware, resulting in all subsequent handlers running inside the CLS call stack and having access to your session variables:

app.use(async (req, res, next) => {
   const authInfo = await extractAuthInformation(req.headers);
  
   return new Promise((res, reject) => {
     clsSession.run(() => {
         clsSession.set("ctx", authInfo);
         next();
         res();
       });
   });
});

We can also apply the same idea for Apollo middleware:

const contextMiddleware: IMiddleware = (resolve, root, args, ctx, info) => {
    return new Promise((res, reject) => {
      clsSession.run(async () => {
          clsSession.set("ctx", ctx);
          const result = await resolve(root, args, ctx, info);
          res(result);
              
       });
    });
  };

And that’s it! Using this technology allows us to leverage middleware in a more generic way, allowing us to lean into auto-generators like typography, and not needing to write custom resolvers for each of our data models, allowing us to spend more time on features and less time on boilerplate!

You can find an example project using this technique here: https://github.com/cerebruminc/prisma-middleware-cls-hooked-example.

Share this post