DEV Community

Cover image for Mocking local dev and tests with msw + @mswjs/data
Jerico Pingul
Jerico Pingul

Posted on • Edited on

Mocking local dev and tests with msw + @mswjs/data

Background

Recently, I found myself needing to mock CRUD operations from an API. At that time, the API was being developed by another engineer. We agreed on the API specs which allowed me to progress on building the UI.

During development, the mocked APIs are useful to build to mock the actual API implementation.

During testing, it is also valuable to be able to test the actual user interactions. There are amazing blog posts by Kent C. Dodds (author of @testing-library/react) on avoiding testing implementation details and mocking the actual API over mocking fetch.

In this article, we will go though the approach I went to building this mock server using msw by building a simple pet dog CRUD application, that has the following features:

  • List all dogs
  • Create a dog
  • Update a dog
  • Delete a dog

Additionally, data can be stored in-memory database provided by a standalone data library msw/datajs. This provides the capabilities of describing our data, persisting them in-memory and read/write operations. We will explore writing REST API handlers backed by the data library methods.

Setup

In this article, l will be building a simple CRUD React application. To help quickly bootstrap my application I will be using the vitejs react-ts template and Chakra UI components. To help simplify and abstract our data-fetching and manage server state, react-query will be used.

For this demo, we will need to install the msw libraries and a mock generator faker. At the time of writing, the latest version of faker has “endgamed”. For this post, we’ll use version 5.5.3, which still works.

yarn add msw @mswjs/data
yarn add faker@5.5.3
Enter fullscreen mode Exit fullscreen mode

Data model

Models are blueprint of data and entities are instances of models. Each model requires a primary key that is a unique ID in a traditional database.

Here, we define our dog model. Each property in the model definition has an initializer that seeds a value and infers the type. Each model must have a primary key that is a unique ID, that we may be familiar with in traditional databases.

import { factory, primaryKey } from '@mswjs/data';
import faker from 'faker';

const modelDictionary = {
  dog: {
    id: primaryKey(faker.datatype.uuid),
    breed: () => faker.helpers.randomize(BREEDS),
    age: () => faker.datatype.number(13),
    description: () => faker.lorem.words(5),
    owner: () => `${faker.name.firstName()} ${faker.name.lastName()}`,
  },
};

const db = factory(modelDictionary);
Enter fullscreen mode Exit fullscreen mode

Seeding data

Once the database is created, we can seed it with data. Properties that aren’t set in the .create method will be resolved by the model dictionary definition.

export function seedDb() {
  db.dog.create({ owner: 'Jerico', breed: 'maltese' });
  db.dog.create({ owner: 'Jerry', breed: 'pug' });
}
Enter fullscreen mode Exit fullscreen mode

Request handlers

These are functions that will mock the API requests from our app. In this app, we will be using the rest handlers to mock our REST API. More information on the syntax can be found in the msw docs.

export const handlers = [
  rest.get<DefaultRequestBody, PathParams, Dog[]>(
    '/api/dogs',
    (_req, res, ctx) => {
      return res(ctx.json(db.dog.getAll()));
    }
  ),
  rest.post<Omit<Dog, 'id'>, PathParams, Dog>('/api/dogs', (req, res, ctx) => {
    const created = db.dog.create(req.body);
    return res(ctx.json(created));
  }),
  rest.delete<DefaultRequestBody, { id: string }, Dog>(
    '/api/dogs/:id',
    (req, res, ctx) => {
      db.dog.delete({ where: { id: { equals: req.params.id } } });
      return res(ctx.status(204));
    }
  ),
  rest.put<Omit<Dog, 'id'>, { id: string }, Dog>(
    '/api/dogs/:id',
    (req, res, ctx) => {
      const updated = db.dog.update({
        where: { id: { equals: req.params.id } },
        data: req.body,
      });
      return res(ctx.json(updated!));
    }
  ),
];
Enter fullscreen mode Exit fullscreen mode

Alternatively, mswjs/data provides a neat method that actually generates these request handlers using the following. Do note that the generated routes are in the following conventional format.

const handlers = [...db.user.toHandlers('rest')]
Enter fullscreen mode Exit fullscreen mode

Running msw

In the browser

In our source code we can execute the following line. Note that we may want to conditionally execute this only on our local dev server.

import { setupWorker } from 'msw';

setupWorker(...handlers).start();
Enter fullscreen mode Exit fullscreen mode

In the tests

Similarly, to mock API requests in our tests:

import { setupServer } from 'msw/node';

const server = setupServer(...handlers);

beforeAll(() => {
  server.listen();
});

afterAll(() => {
  server.close();
});
Enter fullscreen mode Exit fullscreen mode

Implementation

The implementation will not be included in this post, but the full source code can be found in my repo and deployed here.

Wrap up

Writing a mock API using msw and mswjs/data allowed me to develop the UI while the actual API was being developed by another engineer. This setup also allowed me to write the request handlers only once for both my development server and tests. This personally made the effort worthwhile and made writing my tests enjoyable.

I hope this is something that will be of benefit to you, as much as it was for me.

Further reading

In a more complex application, we could have multiple data models and can have relationships with each other. mswjs/data allows establishing relationships between our models in the docs here.

Additionally, there are more model methods to explore. I like the way the API is likened to SQL and take inspiration from prisma.io.

mswjs/data supports GraphQL as well, which I’d love to explore in my next project.

Top comments (0)