MSW Handlers with Filesystem Route

February 11, 2023

When we use Mock Service Worker (MSW) for api mocking, as the number of handlers grows, it's common we come into these problems.

First, we often have to write the same API path multiple times for register handlers for each method it supports as REST API.

// mocks/handlers.js
const handlers = [
  rest.get("/api/todos", "..."),
  rest.post("/api/todos", "..."),
  rest.get("/api/todos/:id", "..."),
  rest.put("/api/todos/:id", "..."),
  rest.delete("/api/todos/:id", "..."),

  // It's even worse when paths are nested
  rest.get("/api/todos/:id/attachments", "..."),
  rest.post("/api/todos/:id/attachments", "..."),
  rest.get("/api/todos/:id/attachments/:name", "..."),
  rest.put("/api/todos/:id/attachments/:name", "..."),
  rest.delete("/api/todos/:id/attachments/:name", "..."),
]

And second, it's heavy to put all these handlers in a single file, so we usually separate them as modules. Most of time we separate modules based on features, which often couple with the REST API paths. And again that's repetition.

Is there a way to define modules directly using API paths, just like Next.js API Routes? For example, can we create an api/todos.js and export a { post, get } object, and it generates POST /api/todos and GET /api/todos handlers for us?

// mocks/api/todos.js
const handlers = {
  get: (req, res, { json }) => res(json("...")),
  post: (req, res, { status, json }) => res(status(201), json("...")),
}

export default handlers

Fortunately, if you are building with webpack, you can do this with require.context.

require.context allow us to dynamicly require files under a directory, like:

// Search for all files ending with .js under ./api directory,
// including files in sub-directories
require.context("./api", true, /\.js$/)

That enables us to implement API Routes with it:

// mocks/handlers.js
const requireMockHandler = require.context("./api", true, /\.js$/)

export const handlers = requireMockHandler
  .keys()
  .reduce((allHandlers, path) => {
    const handlers = requireMockHandler(path).default

    // map "./path/[to]/handlers.js" -> "/api/path/:to/handlers"
    const apiPath =
      "/api/" +
      path
        // trim "./" at start
        .replace(/^\.\//, "")
        // trim ".js" at end
        .replace(/\.[^/.]+$/, "")
        // replace "[param]" with ":param"
        .replace(/\[([^/]*)\]/g, ":$1")

    const pathHandlers = Object.getOwnPropertyNames(handlers).map(method => {
      const handler = handlers[method]

      // for every method in handlers, register an msw handler
      if (method in rest) {
        return rest[method](apiPath, handler)
      }

      return undefined
    })

    return [...allHandlers, ...pathHandlers]
  }, [])

Finally, all we have to do is to create files according to API paths, and get msw handlers without repeatedly writing down API paths.

src/mocks/
├── api/
|   ├── todos.js
|   └── todos/
|       ├── [id].js
|       └── [id]/
|           ├── attachments.js
|           └── attachments/
|               └── [name].js
└── handlers.js

The example code snippets demoed in this article can be found at https://github.com/SevenOutman/msw-require-context-demo


Profile picture

Written by Doma who just migrated his blog to Gatsby.js. You should follow him on Twitter and GitHub.