DEV Community

alextrastero
alextrastero

Posted on • Updated on

Why I migrated to msw from json-server

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)"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

A json-server.json file with the following config:

{
  "host": "localhost",
  "port": 4000,
  "delay": 250
}
Enter fullscreen mode Exit fullscreen mode

And a package.json script:

"api": "json-server demo/db.json",
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

routes.json:

{
  "/campaigns/:campaign_id/tasks": "/campaigns_tasks"
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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()
  }));
};
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

handlers.js

export const handlers = [
  ...
]
Enter fullscreen mode Exit fullscreen mode

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)
    );
  },
]
Enter fullscreen mode Exit fullscreen mode

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,
]
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (2)

Collapse
aleixsuau profile image
Aleix Suau

Great and fun post Alex, thanks!

Collapse
kettanaito profile image
Artem Zakharchenko

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.