In the previous chapter, we built the application pages that use test data. The UI of the pages is complete, but the pages are not functional yet. We are using the test data without making requests to the API.
In this chapter, we will learn what mocking is and why it is useful. We will learn how to mock the API endpoints with the msw library, a great tool that allows us to create mocked API endpoints that behave as real-world API endpoints.
We will also learn how to model the data of our application entities with the @mswjs/data library.
In this chapter, we will cover the following topics:
By the end of this chapter, we will learn how to generate fully featured mocked APIs with data models set in place, which will make our code base less dependent on external APIs during development.
Before we get started, we need to set up our project. To be able to develop our project, we will need the following things installed on our computer:
There are multiple ways to install Node.js and npm. Here is a great article that goes into more detail: https://www.nodejsdesignpatterns.com/blog/5-ways-to-install-node-js.
The code files for this chapter can be found here: https://github.com/PacktPublishing/React-Application-Architecture-for-Production
The repository can be cloned locally with the following command:
git clone https://github.com/PacktPublishing/React-Application-Architecture-for-Production.git
Once the repository is cloned, we need to install the application’s dependencies:
npm install
We can provide the environment variables using the following command:
cp .env.example .env
Once the dependencies have been installed, we need to select the right stage of the code base that matches this chapter. We can do that by executing the following command:
npm run stage:switch
This command will prompt us with a list of stages for each chapter:
? What stage do you want to switch to? (Use arrow keys) ❯ chapter-02 chapter-03 chapter-03-start chapter-04 chapter-04-start chapter-05 chapter-05-start (Move up and down to reveal more choices)
This is the fifth chapter, so we can select chapter-05-start if we want to follow along, or chapter-05 to see the final results of the chapter.
Once the chapter has been selected, all files required to follow along with the chapter will appear.
For more information about the setup details, check out the README.md file.
Mocking is the process of simulating parts of the system, meaning they are not production-ready but fake versions that are useful for development and testing.
You may ask yourself, Why do we want to bother with setting a mocked API? There are several benefits of having the API mocked:
For testing our API endpoints, we will use the Mock Service Worker (MSW) library, a great tool that allows us to mock endpoints in a very elegant way.
MSW is a tool that allows us to create mocked APIs. It works as a service worker that intercepts any API request that has its mocked version defined. We can inspect the requests and responses in the Network tab of our browser the same way as if we were calling the real API.
To get a high-level overview of how it works, let’s take a look at the diagram provided on their website:
Figure 5.1 – MSW workflow diagram
One great thing about MSW is that our application will behave the same as if it were using a real API, and it’s pretty trivial to switch to using the real API by turning mocked endpoints off and not intercepting requests.
Another great thing is that since the interception happens at the network level, we will still be able to inspect our requests in the Network tab of the browser dev tools.
We already have the MSW package installed as a dev dependency. The msw mocked API can be configured to work both in the browser and on the server.
The browser version of the mocked API can be used for running mocked endpoints during the development of the application.
The first thing that needs to be done is to create a service worker. This can be done by executing the following command:
npx msw init public/ --save
The preceding command will create a service worker at public/mockServiceWorker.js, which will intercept our requests in the browser and modify the responses accordingly.
We can now configure our worker to use the endpoints we will define in a couple of moments. Let’s open the src/testing/mocks/browser.ts file and add the following:
import { setupWorker } from 'msw'; import { handlers } from './handlers'; export const worker = setupWorker(...handlers);
The preceding snippet will configure MSW to work with the provided handlers in the browser.
The server version is used mostly during running automated tests since our test runner works in the Node environment instead of the browser. The server version is also useful for API calls executed on the server, which we will have for our application during server-side rendering.
Let’s open the src/testing/mocks/server.ts file and add the following:
import { setupServer } from 'msw/node'; import { handlers } from './handlers'; export const server = setupServer(...handlers);
The preceding snippet will apply the handlers to the server version of our mocks.
Now that we have configured MSW, we need to make it run in our application. To do that, let’s open the src/testing/mocks/initialize.ts file and modify the initializeMocks function to the following:
import { IS_SERVER } from '@/config/constants'; const initializeMocks = () => { if (IS_SERVER) { const { server } = require('./server'); server.listen(); } else { const { worker } = require('./browser'); worker.start(); } }; initializeMocks();
The initializeMocks function is in charge of calling the proper MSW setup based on the environment it is being called in. If it is executed on the server, it will run the server version. Otherwise, it will start the browser version.
Now, we need to integrate our mocks.
Let’s create an src/lib/msw.tsx file and add the following:
import { MSWDevTools } from 'msw-devtools'; import { ReactNode } from 'react'; import { IS_DEVELOPMENT } from '@/config/constants'; import { db, handlers } from '@/testing/mocks'; export type MSWWrapperProps = { children: ReactNode; }; require('@/testing/mocks/initialize'); export const MSWWrapper = ({ children, }: MSWWrapperProps) => { return ( <> {IS_DEVELOPMENT && ( <MSWDevTools db={db} handlers={handlers} /> )} {children} </> ); };
Here we defined MSWWrapper, which is a component that will wrap our application and initialize MSW and the MSW dev tools into the wrapped application.
Now we can integrate it into our application by opening src/pages/_app.tsx.
We want to add new imports:
import dynamic from 'next/dynamic'; import { API_MOCKING } from '@/config/constants'; import { MSWWrapperProps } from '@/lib/msw';
Then, we want to load MSWWrapper dynamically:
const MSWWrapper = dynamic<MSWWrapperProps>(() => import('@/lib/msw').then(({ MSWWrapper }) => MSWWrapper) );
Finally, let’s modify the return statement of our App component as follows:
return ( <AppProvider> {API_MOCKING ? ( <MSWWrapper>{pageContent}</MSWWrapper> ) : ( pageContent )} </AppProvider> );
As you can see, we will load the MSWWrapper component and wrap the content of the page only if the mocks are enabled. We are doing this to exclude MSW-related code for the production version of the application, which uses the real API and does not need the redundant MSW-related code.
To verify that MSW is running, let’s open the console. We should see something like this:
Figure 5.2 – MSW running in our application
Now that we have successfully installed and integrated MSW in our application, let’s implement our first mocked endpoint.
To define mocked endpoints, we need to create request handlers. Think of request handlers as functions that determine whether a request should be intercepted and modified by mocking their responses.
Let’s create our first handler in the src/testing/mocks/handlers/index.ts file by adding the following:
import { rest } from 'msw'; import { API_URL } from '@/config/constants'; export const handlers = [ rest.get(`${API_URL}/healthcheck`, (req, res, ctx) => { return res( ctx.status(200), ctx.json({ healthy: true }) ); }), ];
We are using the rest helper provided by msw to define our rest endpoints. We are using the get method, which accepts the path, and a callback, which will modify the response.
The handler callback will return a response with a status code of 200 and the response data set as { healthy: true }.
To verify that our handler is working, let’s open the dev tools in the bottom-right corner and then select the health check endpoint:
Figure 5.3 – Health check handler test selection
Sending the request should give us a response, as shown here:
Figure 5.4 – Health check handler test result
The Devtools widget is going to give us the ability to test our handlers without creating the UI in the application right away.
Now that we have MSW running properly in our application, it is time to create a data model for our application.
For data modeling our application, we will be using the data library from MSW, which is very useful and simple to use to manipulate the data in a similar way an object-relational mapper (ORM) on the backend would.
To make our request handlers functional, we could just hardcode the responses, but where is the fun in that? With MSW and its data library, we can build a mocked backend that is fully functional with the business logic included should we decide to implement it.
To configure our data models, let’s open the src/testing/mocks/db.ts file and add the following:
import { factory, primaryKey } from '@mswjs/data'; import { uid } from '@/utils/uid'; const models = { user: { id: primaryKey(uid), createdAt: Date.now, email: String, password: String, organizationId: String, }, organization: { id: primaryKey(uid), createdAt: Date.now, adminId: String, name: String, email: String, phone: String, info: String, }, job: { id: primaryKey(uid), createdAt: Date.now, organizationId: String, position: String, info: String, location: String, department: String, }, }; export const db = factory(models);
We are importing the factory and primaryKey functions from the @mswjs/data package. The primaryKey function allows us to define primary keys in our mocked database, and the factory function creates an in-memory database that we can use for testing.
We can then access a bunch of different methods on each model that allow us to manipulate our data more easily, as follows:
db.job.findFirst db.job.findMany db.job.create db.job.update db.job.delete
It would also be great if we could pre-fill some data in the database so that we always had something to show in our application. To do that, we should seed the database.
Let’s open the src/testing/mocks/seed-db.ts file and add the following:
import { db } from './db'; import { testData } from '../test-data'; export const seedDb = () => { const userCount = db.user.count(); if (userCount > 0) return; testData.users.forEach((user) => db.user.create(user)); testData.organizations.forEach((organization) => db.organization.create(organization) ); testData.jobs.forEach((job) => db.job.create(job)); };
The seedDb function will populate the database with test data.
Let’s execute it after our mocked endpoints have been initialized. Open src/testing/mocks/initialize.ts and call the function, as follows:
import { IS_SERVER } from '@/config/constants'; import { seedDb } from './seed-db'; const initializeMocks = () => { if (IS_SERVER) { const { server } = require('./server'); server.listen(); } else { const { worker } = require('./browser'); worker.start(); } seedDb(); }; initializeMocks();
To inspect the data in our database, we can open the Data tab in Devtools:
Figure 5.5 – Inspecting seeded data
Awesome! Now, we have our database pre-filled with some test data. We can now create request handlers, which will interact with the database and consume the data.
In this section, we will define handlers for our application. As already mentioned, a handler in MSW is a function that, if defined, will intercept any matching requests, and instead of sending the requests to the network, it will modify them and return the mocked response.
Before getting started, let’s take a quick look at the src/testing/mocks/utils.ts file, which includes some utilities we will be using for handling the business logic of our API handlers:
Before getting started, let’s include all handlers in the configuration. Open the src/testing/mocks/handlers/index.ts file and change it to the following:
import { rest } from 'msw'; import { API_URL } from '@/config/constants'; import { authHandlers } from './auth'; import { jobsHandlers } from './jobs'; import { organizationsHandlers } from './organizations'; export const handlers = [ ...authHandlers, ...jobsHandlers, ...organizationsHandlers, rest.get(`${API_URL}/healthcheck`, (req, res, ctx) => { return res( ctx.status(200), ctx.json({ healthy: true }) ); }), ];
We are including all handlers we will define in each of the handlers’ files and make them available to MSW.
Now, we can start working on the request handlers for our application.
For the auth feature, we will need the following endpoints:
The endpoints for auth will be defined in the src/test/mocks/handlers/auth.ts file.
Let’s start by importing the dependencies:
import { rest } from 'msw'; import { API_URL } from '@/config/constants'; import { authenticate, requireAuth, AUTH_COOKIE, } from '../utils';
Then, let’s create a request handler for logging in:
const loginHandler = rest.post( `${API_URL}/auth/login`, async (req, res, ctx) => { const credentials = await req.json(); const { user, jwt } = authenticate(credentials); return res( ctx.delay(300), ctx.cookie(AUTH_COOKIE, jwt, { path: '/', httpOnly: true, }), ctx.json({ user }) ); } );
We are extracting the credentials and using them to get the user info and the token. Then, we attach the token to the cookie and return the user in the response with a 300 ms delay as the real API.
We are using the httpOnly cookie because it is safer since it is not accessible from the client.
Then, let’s create a logout handler:
const logoutHandler = rest.post( `${API_URL}/auth/logout`, async (req, res, ctx) => { return res( ctx.delay(300), ctx.cookie(AUTH_COOKIE, '', { path: '/', httpOnly: true, }), ctx.json({ success: true }) ); } );
The handler will just empty the cookie and return the response. Any subsequent requests to protected handlers will throw an error.
Finally, we have an endpoint for fetching the currently authenticated user:
const meHandler = rest.get( `${API_URL}/auth/me`, async (req, res, ctx) => { const user = requireAuth({ req, shouldThrow: false }); return res(ctx.delay(300), ctx.json(user)); } );
The endpoint will extract the user from the token and return it in the response. In the end, we should export the handlers in order for them to be consumed by MSW:
export const authHandlers = [ loginHandler, logoutHandler, meHandler, ];
For the jobs feature, we will need the following endpoints:
The endpoints for jobs will be defined in the src/test/mocks/handlers/jobs.ts file.
Let’s start by importing the dependencies:
import { rest } from 'msw'; import { API_URL } from '@/config/constants'; import { db } from '../db'; import { requireAuth } from '../utils';
Then, let’s implement a handler for fetching jobs:
const getJobsHandler = rest.get( `${API_URL}/jobs`, async (req, res, ctx) => { const organizationId = req.url.searchParams.get( 'organizationId' ) as string; const jobs = db.job.findMany({ where: { organizationId: { equals: organizationId, }, }, }); return res( ctx.delay(300), ctx.status(200), ctx.json(jobs) ); } );
We are getting the organization ID from the search parameters and using that to get jobs for a given organization, which we return in the response.
Another endpoint we want to create is a job details endpoint. We can do that by creating the following handler:
const getJobHandler = rest.get( `${API_URL}/jobs/:jobId`, async (req, res, ctx) => { const jobId = req.params.jobId as string; const job = db.job.findFirst({ where: { id: { equals: jobId, }, }, }); if (!job) { return res( ctx.delay(300), ctx.status(404), ctx.json({ message: 'Not found!' }) ); } return res( ctx.delay(300), ctx.status(200), ctx.json(job) ); } );
We are getting the job ID from the URL parameters and using that to retrieve the given job from the database. If the job is not found, we return a 404 error. Otherwise, we return the job in the response.
Our application also needs an endpoint for creating jobs. We can create a handler for that, as follows:
const createJobHandler = rest.post( `${API_URL}/jobs`, async (req, res, ctx) => { const user = requireAuth({ req }); const jobData = await req.json(); const job = db.job.create({ ...jobData, organizationId: user?.organizationId, }); return res( ctx.delay(300), ctx.status(200), ctx.json(job) ); } );
We are first checking whether the user is authenticated since we don’t want to allow creation for unauthenticated users. Then, we get the job data from the request and use that to create a new job, which we then return in the response.
Finally, we want to export the handlers to make them available for MSW:
export const jobsHandlers = [ getJobsHandler, getJobHandler, createJobHandler, ];
For the organizations feature, we will need the GET /organizations/:organizationId endpoint.
All handlers for this feature will be defined in the src/test/mocks/handlers/organizations.ts file.
Let’s start by importing all required dependencies:
import { rest } from 'msw'; import { API_URL } from '@/config/constants'; import { db } from '../db';
Then, we can implement the endpoint for getting organization details by adding the following:
const getOrganizationHandler = rest.get( `${API_URL}/organizations/:organizationId`, (req, res, ctx) => { const organizationId = req.params .organizationId as string; const organization = db.organization.findFirst({ where: { id: { equals: organizationId, }, }, }); if (!organization) { return res( ctx.status(404), ctx.json({ message: 'Not found!' }) ); } return res( ctx.delay(300), ctx.status(200), ctx.json(organization) ); } );
We are getting the organization ID from URL parameters and using that to retrieve the given organization. If it doesn’t exist in the database, the handler will return a 404 error; otherwise, it will return the found organization.
In the end, we must export the handler:
export const organizationsHandlers = [ getOrganizationHandler, ];
To verify that we have all handlers defined, we can visit Devtools again:
Figure 5.6 – Mocked endpoints
Great! Now, we have all the required handlers to make our application work as if it were consuming the real API. Play around with the handlers to make sure that everything works as expected. In the next chapter, we will integrate these endpoints into the application.
In this chapter, we learned about mocking the API. We introduced the MSW library, which is a great tool for mocking APIs in an elegant way. It can work both in the browser and on the server. It is super useful for prototyping and testing the application during development.
In the next chapter, we will integrate the API layer of the application, which will consume the endpoints we just created.