Middleware for the mock-service-worker

The mock-service-worker can be used to mock backends. You can use it in tests, but also in demos. Unlike real http-server frameworks like express.js, the mock-service-worker does not have that notion have "middleware", and I struggled a bit my mock-authentication for the Gachou demo implemented... Here is a solution, and may be feature-suggestion for the Mock Service Worker team.

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.

Don’t be confused

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.