How to develop an offline Front-End app with mock data


Have you ever wondered how to develop a Front-End project without relying on the API being ready and available to fetch data from?

In this tutorial, I’ll share different approaches to how you can do just that, starting by setting up a mock server and then showing how to integrate the mock server into multiple front-end environments — development, test, and component development environments.

Here we go!

Previous experience with mock servers

From previous experience, I’ve used mock servers based on Express.js where I would spin up the mock server and then any other environment server that would target HTTP requests to it.

Although this pattern is very flexible in the way we can use any 3rd party packages we want inside our mock server, it wasn’t always the best fit for being used across the front-end stack environments.

As an example, one common tool we use on front-end projects to develop components is Storybook, and when we get to the part where we assemble pages, more often than not, we have data requirements that need to be fetched from somewhere.

Some of this data can be stored locally on the project and delivered by its own mock server, but what about data coming from 3rd party services?

This case is something that an Express.js mock server won’t cover if your codebase is making the requests directly to the 3rd party services. One way to get around this is by making custom endpoints on the mock server that mock the response from those same 3rd party services. This implies one layer of abstraction on our mock server that may not match our codebase data fetching strategy.

A front-end project should behave the exact same way with or without a mock server. If we start introducing conditionals on our code to handle both scenarios, that’s one extra piece of code we need to maintain.

There is another scenario where using an Express.js mock server for our Storybook environment isn’t a perfect fit. We can use it just fine on our local machines, but what about deploying our Storybook project to something like Chromatic?

When developing locally, we run both the Storybook development server and the Express.js mock server as well, but the same won’t happen when we deploy it.

To deploy a Storybook project, we first build the production version of it, which is a static SPA version of it. This static build is deployed to any server that can deliver static assets, but the mock server isn’t deployed with it.

It would be great to have a mock server that can be used both locally and in “production” environments without any extra setup/configuration.

Enter Mock Service Worker (MSW), a library that provides a seamless API for mocking on the browser and Node.js. It’s being marketed as an “API mocking of the next generation” on their website, and it does the mocking by intercepting requests on the network level.

It mentions that it can be used to reuse the same mock definition for testing, development seamlessly, and debugging.

This kind of seamless integration is what we are aiming for today!

The end goal is to start with the repo from the How to set up a front-end project with Vite, React, and TypeScript blog post as a baseline, add new components with external data requirements and mock those exact requirements with MSW.

MSW for the win

To get us started, we’re going to build upon the project created in the How to set up a front-end project with Vite, React, and TypeScript blog post:

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=01-initial-setup.sh

This will clone the repository into a folder called project-demo-msw and install the NPM dependencies as well.

New component with external data requirements

Let’s create a new component that fetches some data from an external API and then renders some UI accordingly.

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=02-create-users-component.sh

Here’s a simple React component with static HTML that you can copy to the src/components/users.tsx file:

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=03-users-component.tsx

Before we continue making more changes, let’s first create a new Story so we can preview this component in the Storybook environment:

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=04-create-users-component-story.sh

Now you can copy the code below to configure the Story file with our new Users component:

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=05-users-component-story.tsx

It’s time to run our Storybook development environment and see our component being rendered:

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=06-run-storybook.sh

Preview of the Users component on Storybook


Now we’re going to update the component to make an HTTP request to an API to fetch some fake users’ data to replace our current static users’ list.

The API that we are going to use is called JSONPlaceholder, and more information about it can be found here.

To make the HTTP request, we will use the browser Fetch API. We will also need to store the data coming from the API in our React component. For that, we will use React’s useState hook to manage our reactive state and the useEffect hook to integrate our async HTTP request code into React.

For more information on making an HTTP request inside React, you can check this documentation.

Here’s what our src/components/users.tsx component looks like with the API request in place:

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=07-update-users-component.tsx

We fetch the users’ data from the JSONPlaceholder API and then save it to the users’ state variable by calling the setUsers method with the response result.

Preview of the Users component with data coming from the JSONPlaceholder API


Now we’re rendering the users that the JSONPlaceholder API returned, but what if the API request simply fails?

We can trigger this scenario by making our browser behave in an offline manner if we switch the network condition to offline on the developer tools. The following example is on the Brave Browser, which is Chromium-based, so others Like Google Chrome, Microsoft Edge, Opera, etc. should be verified as similar:

We get some errors on the browser console saying there's no internet connection, which we intended to do.

This is where a mock server can help us developers stay productive by giving us a way to mock our data requirements in order for us to keep developing the component UI first and deal with this sort of scenario afterward.

Install MSW


https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=08-install-msw.sh

This will install the MSW package as a dev dependency on the project, and then it will copy over a service worker file called mockServiceWorker.js to the public folder on the root of the repository.

Notice that we install a specific version of MSW to be 100% compatible with the rest of the tutorial.

Mocking external API

To define which requests should be mocked, we will use request handler functions. They allow us to capture any request based on its method, URL, or other criteria and specify which response to return.

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=09-create-mocks-handlers.sh

The handlers from this module can be reused for both browser and Node in the further steps of this tutorial.

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=10-mocks-handlers.ts

Now that we have a mock for the JSONPlaceholders API users endpoint, we need a way to use it inside the Storybook environment.

Setup MSW for Storybook

We can now set up our Storybook environment to initialize MSW on page load. To integrate MSW into Storybook, a package called msw-storybook-addon makes this process a breeze.

Let’s start by installing the package itself:

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=11-install-msw-storybook-addon.sh

We can now configure Storybook with the package installed to initialize the service worker when loading Stories. Let’s update the .storybook/preview.js file to look something like:

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=12-storybook-preview.js

We added the import statements for the msw-storybook-addon package as well for the MSW handlers variables that we created in the previous step.

We start the service worker on line 5 and pass the configuration option onUnhandledRequest with the value bypass, so we don't have warnings on the console regarding HTTP requests not being handled by MSW.

On line 7, we define a global decorator that integrates seamlessly with our Story files for when we want to configure specific MSW handlers for specific stories.

Last but not least, on line 17, we configure our global handlers. This way, we don’t need to add the handlers to a specific story, as it will be loaded automatically.

We need to do one more thing before testing our mocks in Storybook. We talked about a service worker being initialized, but where is this service worker code available? The service worker file was generated into the public folder from the step where we installed the MSW package.

Assets in this directory will be served by Vite at the root path / during development and copied to the root of the dist directory as-is when building. To get the same behavior on Storybook, we need to update the .storybook/main.js file to serve the public folder assets as static files as well:

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=13-storybook-main.js

Now we can restart our Storybook development server and check if the MSW is being initialized correctly:

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=14-run-storybook.sh

After opening the Storybook on the browser and checking the console, we can see a log message saying [MSW] Mocking enabled. which means our MSW setup for Storybook was successful. You can also see another log right after with a message like [MSW] GET https://jsonplaceholder.typicode.com/users (200 OK) that tells us a request to the https://jsonplaceholder.typicode.com/users URL was intercepted correctly.

This is exactly the result we were expecting, but so far, everything is working as it was before installing and setup MSW. It’s time to test the scenario that we tested before, where we switched the network condition to offline on the developer tools:

As you can see on the screenshot above, even after forcing the browser to be offline 🎉, the mock server still works, and the Users component still gets the users’ data that we configured on the handlers file.

This is a big deal as developers can mock our data requirements and keep developing components in total isolation.

Also, remember the issue we talked previously about when we tried to deploy our design system, and then the mock server wasn’t available? This issue is now gone because the mock server powered by MSW will be bundled when we build the Storybook project. This means that when we deploy the Storybook statically built assets, MSW will also be a part of it, and it will work the same way it does when developing locally.

Let’s test this by building our Storybook project first:

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=15-build-storybook.sh

Now there is a storybook-static folder on the root of the repository that can be served with any static HTTP server; for this example, we will use the http-server package:

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=16-serve-storybook-static.sh

This will start a static HTTP server serving our Storybook-built assets. Now we can visit http://localhost:8080 to view our stories.

Everything’s working exactly the way it did when running with the development server! One last test just for good measure — let’s set the browser’s network to be offline and see if we still get the mock data users to be rendered on the Users component.

And with no surprise, it still works. 🎉

Before continuing with the tutorials, let's update the .gitignore file to ignore the storybook-static folder, so we don’t accidentally commit generated files to our repository

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=17-gitignore.txt

Setup MSW for the development environment

With our Storybook environment being 100% to be used with MSW, it’s time to move on to the front-end development server.

We already did a couple of steps required to use MSW with our development environment. One being installing MSW and the other being adding the request handlers. With these both out of the way, we need to initialize the MSW service worker when we load pages with our development server.

To initialize the MSW service worker, we need to configure the worker following this documentation. Let's create a file in our mocks directory (src/mocks) where we would configure and start our service worker.

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=18-create-mocks-browser.sh

In the browser.ts file, we are going to create a worker instance with our request handlers defined earlier.

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=19-mocks-browser.ts

For our mocks to execute during the runtime, it needs to be imported into our application's code. However, since mocking is a development-oriented technique, we will be importing our src/mocks/browser.ts file conditionally, depending on the current environment.

It's not recommended to include Mock Service Worker in production. Doing so may lead to a distorted experience for your users.

Import the src/mocks/browser.ts file conditionally on the src/main.tsx file:

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=20-update-main.tsx

The prepare function was added to import the browser.ts file dynamically only when we are in development mode.

We can now run the development server and see if the MSW service worker gets initialized properly:

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=21-run-dev-server.sh

After opening http://localhost:3000 on the browser and checking the console, we should see a log with [MSW] Mocking enabled. just like we saw when we set up MSW for Storybook.

With our mock server ready to be used on the development server, let’s import the Users component and render it on the page by updating the src/App.tsx file:

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=22-update-app.tsx

As you can see, the request handler for the https://jsonplaceholder.typicode.com/users endpoint was called successfully. 🎉

The MSW is now ready to mock any network request you need!

Setup MSW for the Jest environment

Our project has a couple of test environments configured, one being Jest and the other being Cypress. It would be awesome to use MSW on these environments as well.

Previously we set up MSW to run on Storybook and development server, which are both browser environments, but Jest is a Node environment. Thankfully, MSW handlers can be used on both browsers and Node environments!

To integrate MSW handlers into a Node environment, we need to configure a server for it first. Let's create a file in our mock definition directory (src/mocks) where we would configure our request mocking server:

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=23-create-mocks-server.sh

In the server.ts file, we will create a server instance with our request handlers defined earlier.

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=24-mocks-server.ts

With our mock server ready for Node environments, we can start to integrate with our Jest configuration.

We can create a custom setup module and reference it using Jest's setupFilesAfterEnv option in your jest.config.js file. Let’s create a jest.setup.js test setup file:

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=25-create-jest-setup.sh

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=26-jest.setup.js

Now we need to reference this test setup file in the jest.config.js file:

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=27-jest.config.js

Let’s try running the tests and see if every still works:

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=28-run-tests.sh

We get an error when executing the renders hello message test:

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=29-tests-errors.txt

The specific error message is ReferenceError: fetch is not defined, telling us the Fetch API isn’t available on the Jest Node environment. We can fix this by importing a polyfill for it. For this tutorial, we are going to use the whatwg-fetch package:

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=30-install-fetch-polyfill.sh

Now let’s import this polyfill into the jest.setup.js file created earlier:

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=31-jest-setup-with-fetch-polyfill.js

Let’s rerun our tests:

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=32-run-tests.sh

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=33-tests-success.txt

Our tests run successfully again!

We should now assess that our mock server is indeed working by making some assertions on the App.spec.tsx file. Let's assert that the header with the text Users is being rendered and the users’ list with all 10 items being mocked by the MSW.

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=34-App.spec.tsx

Let’s run the tests again and see if all assertions are true:

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=35-run-tests.sh

We get the following error on the App.spec.tsx test file:

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=36-tests-error.txt

It's telling us that there are no <li> items being rendered when this assertion occurs. This happens because we only render our users list when we have users on the users state variable inside the React component. We make the HTTP request to fetch the users, which takes time to resolve. This is called an asynchronous task, so in sort, we are trying to assert the users' list being rendered before we even have the data (still loading) available to render the UI.

To make our tests more resilient, let’s update our Users component first to have it render a loading state until the users’ data is loaded successfully:

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=37-users-component-with-error-handling.tsx

Let’s update the test file as well to assert on the loading state, and when the loading state is not present anymore, we assert on the users’ list being rendered:

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=38-app-tests-with-loading.tsx

Setup MSW for the Cypress environment

Before we continue with the configuration of MSW for the Cypress environment, let’s update both Vite and Cypress to have the latest and greatest available for us to use:

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=39-update-vite-and-cypress.sh

This command updates Vite and Cypress to their current latest versions.

In the How to set up a front-end project with Vite, React, and TypeScript blog post, we used the Vite development server for running our Cypress tests against. A better approach would be for us to run the production version of our application when running E2E tests.

To run the production version of our application, we first need to build it by running the build command:

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=40-run-build.sh

After our application is built, we can serve it via the serve NPM script:

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=41-run-serve.sh

This starts a server with our previously built assets from the build command.

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=42-run-serve-output.txt

Notice anything different on the URL where the production version of our project is being served? The port is now 4173 instead of 3000, like when we started the development server.

We can configure Vite to use the same port for both development and production by updating the vite.config.js file:

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=43-vite.config.js

Notice the preview and server configuration options added where the port is the same.

Let’s run the serve script again:

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=44-run-serve.sh

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=45-run-serve-output.txt

Our production server is now running on port 3000 as well. This way, we don’t need to change our Cypress test URLs.

To seamlessly integrate MSW with Cypress, we need to make sure it plays well with the Cypress intercept API. This API enables us to spy and stub network requests and responses.

We can use a package called cypress-msw-interceptor to help us with the setup. This plugin takes the features of MSW and adapts its API to work with Cypress in a way that cy.intercept works. This plugin will start an MSW worker as part of the Cypress runner and intercept fetch requests from the tested application.

Let’s start by installing the plugin package:

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=46-install-cypress-msw-interceptor.sh

With the plugin installed, its time to import it on the cypress/support/index.ts file:

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=47-cypress-support-index.ts

Let’s update the integration test file:

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=48-cypress-integration-app.spec.ts

Some errors are being reported by TypeScript, saying that Property 'interceptRequest' does not exist on type 'cy & EventEmitter.’ The method, in fact, exists, but as far as TypeScript is aware, it’s not. To fix this, we can extend the TypeScript declarations for the cy global variable:

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=49-update-cypress-support-index.ts

Let’s run the E2E tests now:

https://gist.github.com/pixelmattersdev/cccb3f82c9e849a01892b2f6913c1f4b?file=50-run-cypress.sh

Cypress dashboard default page


Click on the app.spec.ts integration test to start running it.

Cypress successful run for the app.spec.ts file


The Cypress test is now updated to mock the HTTP request to the JSONPlaceholder API, and you can see on the image above a list of users with the mock data that we added to the app.spec.ts file.

Both Cypress and MSW are amazing technologies; this plugin takes the features of MSW and adapts its API to work with Cypress in a way that cy.route works.

Conclusion

In many projects, waiting for the API is just not an option. And that’s why Mock Service Worker is the perfect match for all the times you need to request data from external resources. Mocks give you a seamless experience that improves your development workflow, as we’ve seen in the different strategies explored in this tutorial.

References

Rui Saraiva
Front-End Developer

Because, every pixel matters.

Let's talk