Middleware for the mock-service-worker

September 04, 2022

Motivation

It may sound a bit crazy, but I have used the mock-service-worker library to run a mock-backend for my Gachou demo, which is hosted completely on Gitlab-pages. You can not do a lot there, just log in and add users. Everything is stored locally in your browser, but the requests are handled by mock-service-worker so that it even looks real in the dev-tool’s network-tab.

I am using the same mock-endpoints in my test-cases and in the demo. In the demo, the endpoints are “secured” (i.e. check the access token), but in the unit tests, I didn’t want to bother with access tokens and login.

The goal

In express.js you would register a middleware that performs the authentication, but mock-service-worker does not support middleware. This is understandable as it is not a full-featured rest-framework. But I thought it should be possible to write a function “allowedRoles” that creates a wrapper for a list of RestHandler instances. Then I could use it like this:

function allowedRoles(
  roles: string[],
  restHandlers: RestHandler[]
): RestHandler[] {
  // for each restHandler, create a wrapped one that calls its "ResponseResolver"
  // after checking the access-token
}

function createUsersHandlers(): RestHander[] {
  return [
    rest.get("/api/users", (req, res, context) => {
      // compute response
    }),
    rest.post("/api/users", (req, res, context) => {
      // store new user
    }),
    rest.get("/api/users/:username", (req, res, context) => {
      // compute response
    }),
    rest.put("/api/users/:username", (req, res, context) => {
      // modify user
    }),
    rest.delete("/api/users/:username", (req, res, context) => {
      // delete user
    }),
  ]
}

// In the demo
setupWorker(...allowedRoles(["ADMIN"], createUsersHandlers()))

// In tests
setupServer(...createusersHandlers())

Unfortunately, in the current version (0.44.2) the RestHandler-class has no way to access the ResponseResolver directly. The class is designed to be created and used directly, not as a “configuration” object that can be wrapped and used in delegates.

I played around with the idea a little, but came to the conclusion that I had to use to many internal and undocumented APIs to achieve my goal.

The solution

Instead, my solution was to not create RestHandler instances directly, but first create a configuration object.

export interface RestHandlerConfig<
  Resolver = ResponseResolver<RestRequest, RestContext, Record<string, unknown>>
> {
  path: string
  method: "PUT" | "POST" | "PATCH" | "DELETE" | "GET"
  resolver: Resolver
}

I didn’t use the rest.get method directly, but created another object with a custom get, post, put, delete and patch method, which created a RestHandlerConfig object instead of a RestHandler. Then I create my own setupServer function:

function createHandler(config: RestHandlerConfig): RestHandler {
  return new RestHandler(config.method, config.path, config.resolver)
}

export function mySetupServer(...configs: RestHandlerConfig[]): SetupServerApi {
  return setupServer(...handlerConfigs.map(createHandler))
}

Finally, I was able to write a wrapper function for my RestHandlerConfig

export function allowedRoles(
  allowedRoles: Role[],
  handlerConfigs: RestHandlerConfig[]
): RestHandlerConfig[] {
  return handlerConfigs.map(({ method, path, resolver }) => {
    return {
      method,
      path,
      resolver: (req, res, context) => {
        const authorizationHeader = req.headers.get("authorization")
        if (authorizationHeader == null) {
          return res(context.status(401))
        }
        const roles = getRolesFromAuthToken(authorizationHeader)
        if (!hasAnyRole(roles, allowedRoles)) {
          return res(context.status(403))
        }
        return resolver(req, res, context)
      },
    }
  })
}

Where is the code?

The code is inside my Gachou project. I currently don’t want to invest the time to publish an npm-package for this use-case. Contact me, if you are planning to do something like that.

My source code implements more than this feature. It also provides the code generated from an OpenAPI-spec, as I wrote in my last post.

Conclusion

My use-case certainly is somewhat strange. Nevertheless, my main conclusion is that my workaround should be part of the mock-service-worker in the first place. I think it would be a good design, more concise and more flexible to separate configuration interfaces from the actual request-handling.

I may open a feature request for this in the future.


Profile picture

Written by Nils Knappmeier who has been a full-time web-developer since 2006, programming since he was 10 years old.