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
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);
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' });
}
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!));
}
),
];
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')]
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();
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();
});
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)