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