Introduction
At my company we've been using json-server since the beginnand as it started simple. Now we've reached a point where the customization is just not enough without writting a full blown node server with express. So I was advised to have a look at Mock Service Worker (msw), and I can say that now I have all that I need to mock all our api's.
json-server
Level: I'm too young to die
We started out with a handful of api's which were quite simple, this was very easy to handle with json-server
, I created a db.json
file with the api's I wanted to mock:
{
"auth": {
"user_id": 60
},
"campaigns": [
{
"id": 1,
"created_at": "2020-05-12T09:45:56.681+02:00",
"name": "Khadijah Clayton"
},
{
"id": 2,
"created_at": "2020-05-12T09:45:56.681+02:00",
"name": "Caroline Mayer"
},
{
"id": 3,
"created_at": "2020-05-12T09:45:56.681+02:00",
"name": "Vanessa Way"
},
{
"id": 4,
"created_at": "2020-05-12T09:45:56.681+02:00",
"name": "Generation X"
},
{
"id": 5,
"created_at": "2020-05-12T09:45:56.681+02:00",
"name": "Mariam Todd (Mitzi)"
}
]
}
A json-server.json
file with the following config:
{
"host": "localhost",
"port": 4000,
"delay": 250
}
And a package.json
script:
"api": "json-server demo/db.json",
Running this with yarn run api
and hitting localhost:4000/campaigns
would return the list of campaigns, so far so good.
Level: Hey, not too rough
Some api's would be nested under a campaign_id
param i.e. /campaigns/:campaign_id/tasks
. So introducing routes:
json-server.json
:
{
"routes": "demo/routes.json",
"host": "localhost",
"port": 4000,
"delay": 250
}
routes.json
:
{
"/campaigns/:campaign_id/tasks": "/campaigns_tasks"
}
This way any hit to localhost:4000/campaigns/321/tasks
would route to /campaigns_tasks
in my database file.
Level: Hurt me plenty
As you can imagine the database file grew unmanageably big very quick. So introducing middlewares:
json-server.json
:
{
"routes": "demo/routes.json",
"middlewares": "demo/middleware.js",
"host": "localhost",
"port": 4000,
"delay": 250
}
middleware.js
:
import campaigns from './demo/campaigns.json';
module.exports = function (req, res, next) {
if (req.method === 'DELETE' || req.method === 'PUT') {
return res.jsonp();
}
if (req.originalUrl === '/campaigns') {
return res.jsonp(campaigns);
}
next();
}
This allowed me to separate data into several json chunks and allowed me to handle other methods like DELETE
or PUT
without the actions editing the database.
Level: Ultra-Violence
However the app continued growing and so would the amount of api's backend would deliver that I wanted mocked. So I updated the middleware to handle the urls with regex in order to fine tune the response.
middleware.js
:
import campaign from './demo/campaign.json';
import tasks from './demo/tasks.json';
module.exports = function (req, res, next) {
if (req.method === 'DELETE' || req.method === 'PUT') {
return res.jsonp();
}
if (req.originalUrl.match(/\/campaigns\/[0-9]*$/)) {
return res.jsonp(campaign);
}
if (req.originalUrl.match(/\/campaigns\/([0-9]+)\/tasks/)) {
return res.jsonp(tasks);
}
next();
}
Level: Nightmare!
As the middleware grew larger so did each individual json file, long arrays of hundreds of items were very hard to maintain. So in order to have the data short and dynamic I added Faker.js.
middleware.js
:
import campaign from './demo/campaign.js';
module.exports = function (req, res, next) {
if (req.originalUrl.match(/\/campaigns\/[0-9]*$/)) {
const data = campaign();
return res.jsonp(data);
}
next();
}
campaigns.js
:
import faker from 'faker';
const gen = (fn) => {
const count = faker.random.number({ min: 1, max: 10 });
return new Array(count).fill(0).map((_, idx) => fn(idx));
};
module.exports = () => {
faker.seed(32);
return gen(() => ({
id: faker.random.number(),
owner_id: faker.random.number(),
active: faker.random.boolean(),
budget: faker.random.number(),
description: faker.lorem.sentence(),
created_at: new Date(faker.date.recent()).toISOString()
}));
};
Interlude
So as you can see, we reached a point where it was being harder and harder to maintain. So at this point I was suggested to try out Mock Service Worker (msw).
MSW
I'm going to skip the setting up part since there are plenty of articles out there 1, 2, 3, 4 to link a few plus of course their own documentation which is 👌🏻.
Config
I do want to mention thought that I have setup both the browser and node types, because I want the browser to handle the api's via service worker and I also want the specs to read from here via node.
server.js
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
// This configures a request mocking server with the given request handlers.
export const server = setupServer(...handlers);
browser.js
import { setupWorker } from 'msw';
import { handlers } from './handlers';
// This configures a Service Worker with the given request handlers.
export const worker = setupWorker(...handlers);
handlers.js
export const handlers = [
...
]
I also had to configure CRA to run the browser.js
on start and jest
to run the server.js
for all tests.
Removing the redundant
Now there's no need to use regular expressions since within the handlers I can setup the REST api logic. So removing middleware.js and routes.json.
handlers.js
import { rest } from 'msw';
import campaigns from './demo/campaigns.js';
import campaign from './demo/campaign.js';
export const handlers = [
rest.get('/campaigns', (_, res, ctx) => {
return res(
ctx.json(campaigns())
);
},
rest.get('/campaigns/:id', (req, res, ctx) => {
const { id } = req.params;
return res(
ctx.json(campaign(id))
);
},
rest.get('/campaigns/:id/*', (req, res, ctx) => {
return res(
ctx.status(200)
);
},
]
You can quickly see that this can be separated into several sections, like campaignHandlers
and others which would make it easier to read.
import campaignHelpers from './handlers/campaigns';
export const handlers = [
...campaignHelpers,
...others,
]
Next steps mswjs/data
The next steps I want to work on when I have the time is setting up the data factories, so that I can create items on demand and have a cleaner structure with models.
Final thoughts
Yes, this article does look more like a json-server
tut, but I thought it might be useful to show the struggles I went through and what made me look for another more versatil solution.
And that's that. Please let me know if you had any similar struggles and if this article was useful to you.
Top comments (4)
Thanks, this was really interesting. I used json-server on my last project and really enjoyed it, but also went through most of the doom referenced stages of grief, mostly as the real apis became less truly restful and we had to hack around the routes to simulate the same thing. Eventually we stopped using the mocks as the real apis came online, but that didn't account for any testing i.e. we were only using mocks because real apis were not yet available.
Am particularly interested in msw for the testing aspect and hopefully less config than json-server, but liked the persistent nature of the data in json-server where if I created, posted, put and deleted data it would actually be removed from the mock DB. The msw and mswjs/data philosophy seems to only deal with data generated on the spot for the purpose of passing/failing a single test, and I catch seem to find anything about creating snapshots of data. Admittedly, I have only been looking at the docs for about 2 seconds, but am wondering if I need to not used mwsjs/data and use some sort of db package? Or maybe I have the wrong mindset and am doing it wrong.
Basically what I am asking is, given a json db structure that was created with faker, can I plug that into something that works with msw?
And if choose to work with msw in production, like a static s3 website, I can? Need to extra config in web pack?
Great and fun post Alex, thanks!
Thank you for sharing this, Alex!
I love your way of showcasing how the requirements toward the API mocking layer grew. Looking forward to reading about your experience with @mswjs/data.