Storybook is one of the best ways to develop UI components for JavaScript applications because it allows previewing components in multiple states, acts as interactive documentation of the code, as well as has a live environment to enable story-first development.
Although presenting small UI units in Storybook is straightforward, when it comes to components that make API requests, developers must reach out for an API mocking solution to get control over responses and take the actual HTTP communication out of the story.
In this article, we are going to integrate an API mocking library called Mock Service Worker into a Storybook project.
Mock Service Worker (MSW) is an API mocking library for browser and Node.js. Apart from the rich support of REST and GraphQL API, the library’s key feature is requests interception on the network level via Service Worker. This means absolutely zero changes made to the component you are testing or developing, as it becomes unaware of any kind of mocking and keeps making the same requests it does in production.
Combined with Storybook, MSW empowers an unrivaled experience of component development by providing a seamless way to control both internal and external API communication. No wonder that MSW is one of the recommended ways to intercept API in Storybook!
We are going to use a new Create React App project. Both Storybook and MSW are framework-agnostic tools, so you can use the steps from this article to integrate them into any other JavaScript project, be it Angular, Vue.js, or Svelte.
You can see the full source code of the project on GitHub.
Let’s start by installing Storybook:
$ npx sb init
Please see the Getting started page in the Storybook’s documentation for more details on the installation.
Once the Storybook is installed, you should see a couple of new directories appearing in your project:
|-- .storybook | |-- main.js | |-- preview.js |-- src | |-- /stories
Next, let’s add the msw
package:
$ npm install msw --save-dev
Mock Service Worker uses a worker script that enables requests interception in a browser. The library comes with a designated CLI to initialize that worker script automatically.
To initialize the worker script, run the npx msw init
command and provide it with a relative path to your project’s public directory, which in the case of create-react-app, is the ./public
folder:
$ npx msw init ./public
Public directory may differ depending on the project. See the list of common public directories for reference.
Our project will be a React component that displays a short detail about a GitHub user. The intention is to render that component like this:
<GitHubUser username="any-username" />
Let’s take a brief look at the source code of the GitHubUser
component:
// src/GitHubUser.jsx import React from 'react' import { useFetch } from '../../../hooks/useFetch' import './GitHubUser.css' export const GitHubUser = ({ username }) => { // Fetch user details from the GitHub API V3. const { data, loading, error, refetch } = useFetch( `https://api.github.com/users/${username}` ) const { name, login, avatar_url } = data || {} // Compose some conditional classes based on the request state. const containerClassNames = [ 'container', loading && 'loading', error && 'error', ] .filter(Boolean) .join(' ') // Eventually, render some markup. return ( <div className={containerClassNames}> <div className="avatar-container"> {avatar_url && <img className="avatar" src={avatar_url} alt={name} />} </div> {error ? ( <div> <p>Failed to fetch a GitHub user.</p> <button onClick={refetch}>Retry</button> </div> ) : ( <div> <p className="name">{name}</p> <p className="username">{login}</p> </div> )} </div> ) }
To fetch the details of a given user, this component calls a GitHub API V3 via a custom useFetch
hook — a tiny abstraction over the native window.fetch
. It also has a nice “retry” functionality in the case when the API call fails.
While that is a valid part of the component’s behavior, the HTTP request it makes doesn’t belong in Storybook. Making actual requests in a story, especially to third-party providers, would establish a tight dependency of our UI on the respective service, preventing the stories we write from being reproducible and disabling the offline usage of Storybook.
Because we are focusing on API mocking in Storybook today, let’s add a story for our GitHubUser
component that showcases its default (successful) behavior:
// stories/GitHubUser.stories.js import { GitHubUser } from '../src/GitHubUser' export default { title: 'GitHub User', component: GitHubUser, } export const DefaultState = () => <GitHubUser username="hamilton.elly" />
Learn more about writing stories in the Storybook documentation.
At this point, the component would render, but still make an actual HTTP request. It’s time to add some API mocking to the mix.
To let MSW know which API calls to mock, we need to declare a set of request handlers — functions that describe request predicates (what requests to capture) and response resolvers (how to respond to those requests). Afterward, the same request handlers can be used to declare a worker for in-browser mocking, or a “server” for mocking in the Node.js environment.
Create an src/mocks
directory in your project to store everything related to API mocking. In that directory, create a file called handlers.js
and declare the request handler for a GET /user/:userId
request following this example:
// src/mocks/handlers.js import { rest } from 'msw' export const handlers = [ // Capture a GET /user/:userId request, rest.get('/user/:userId', (req, res, ctx) => { // ...and respond with this mocked response. return res(ctx.json({})) }), ]
We are declaring request handlers in a separate module because they can be reused for multiple purposes: within your Storybook, during local development, for testing, or for debugging. Write once, reuse anywhere.
When writing mocks, think of MSW as a mocked “server.” Although the library doesn’t establish any actual servers, it acts as one for your application. With that in mind, I recommend keeping the “success” paths of any API in the global mocks/handlers.js
module, while delegating the per-scenario overrides (such as error responses) closer to each individual usage surface (i.e., a specific story, or an integration test).
MSW uses a Service Worker to intercept requests and mock responses in a browser. That’s why we are going to create a worker
instance responsible for that interception.
Use the setupWorker
API and provide it with the previously declared request handlers to register and activate the Service Worker you’ve initialized during the set-up step.
// src/mocks/browser.js import { setupWorker } from 'msw' import { handlers } from './handlers' export const worker = setupWorker(...handlers)
The worker
interface exposes an API to control it (such as start
and stop
methods), but we aren’t going to work with it just yet. Instead, we will delegate that responsibility to Storybook in the next step.
It’s crucial for the tools we use to be resilient to change. That is one of the main reasons to adopt MSW: being request client-agnostic, it allows you to use the same integration even if your application migrates to a different request library tomorrow or a different API convention altogether.
Now, let’s enable API mocking globally in Storybook by editing the .storybook/preview.js
file to conditionally require the worker and start it:
// .storybook/preview.js if (typeof global.process === 'undefined') { const { worker } = require('../src/mocks/browser') worker.start() }
The
global.process
check ensures Storybook doesn’t attempt to activate the Service Worker in a non-browser environment, aspreview.js
also gets executed during the Storybook build that runs in Node.js.
With this step complete, you can see the successful activation message from MSW in the browser DevTools in your story:
You can see that our request has been successfully handled by MSW in both UI and in the Console of DevTools. The best part about this setup is that we didn’t have to change any of our application’s code! It still communicates with GitHub API, but receives the mocked response we’ve specified.
The global request handlers listed in src/mocks/handlers.js
are great for keeping the successful API interactions. However, not all interactions are successful.
If you wish to build a bulletproof UI, you should expect errors and make sure your component can handle them gracefully for a user. Moreover, you should be able to browse through the visual illustrations of your component in multiple network-dependent states in the respective stories.
One of the benefits of Storybook is the ability to showcase a single component in multiple states. In the case of our component, we can illustrate the handling of various HTTP communication scenarios: the loading state while our component awaits the response, and an error response from the GitHub API. For that, you can override request handlers on a per-story basis.
We are going to use story decorators to enhance an individual story with runtime request handlers — an API to append or rewrite handlers during runtime when story renders.
Asynchronous actions may take time, and HTTP calls are not an exception. To guarantee a superb user experience, our component must be able to handle the loading state, while our Storybook should illustrate that loading state in a reproducible and predictable manner.
Luckily, you are in charge of the mocked responses, including their response time. You wouldn’t want, however, to affect unrelated stories, so mocking a loading state in the global request handlers isn’t the best option. Instead, keep the mocking logic for the loading state right next to the story itself. Here’s how you can do that:
// src/stories/Component.story.js import { rest } from 'msw' import { worker } from '../mocks/browser' // Create a new loading state story. const LoadingState = () => <GitHubUser username="hamilton.elly" /> // Use Storybook decorators and MSW runtime handlers // to handle the same HTTP call differently for this particular story. LoadingState.decorators = [ (Story) => { worker.use( rest.get('https://api.github.com/users/:username', (req, res, ctx) => { // Mock an infinite loading state. return res(ctx.delay('infinite')) }) ) return <Story /> }, ]
Notice how we are using a worker.use()
method to provision a runtime request handler. We still provide the same request method and URL, but a different resolver function that delays the response indefinitely (see the ctx.delay
utility). This preserves the response in a pending state, which is exactly what you need to present how your component handles the loading state in the UI.
By inspecting the Network tab in your browser’s DevTools, you can see that the GitHub API request never resolves, allowing us to preview that very state in our story. That’s precisely why we need API mocking here — to gain flexibility and control over the API calls that our components make.
MSW comes with a straightforward API and the variety of utilities to emulate response status codes, headers, server cookies, and many other to enable mocking of real-world scenarios like authentication, CORS, or media content streaming.
Similar to the loading state, you can create a separate story for the error response and have a runtime request handler that always responds with a specific HTTP server error.
// src/stories/Component.story.js import { msw } from 'msw' import { worker } from '../mocks/browser' const ErrorState = () => <GitHubUser username="hamilton.elly" /> ErrorState.decorators = [ (Story) => { worker.use( rest.get('https://api.github.com/users/:username', (req, res, ctx) => { // Respond with a 500 response status code. return res(ctx.status(500)) }) ) return <Story /> }, ]
Use
ctx.status
and other context utilities to model the precise HTTP response you need to showcase your component’s behavior.
Saving the changes and navigating to Storybook, we witness a reproducible error state:
Although our story now shows the error handling, clicking on the Retry button still results in a request that always returns a 500 response, just like we’ve specified in the runtime request handler.
It would be great to return the error response only the first request to GitHub API is made. You can do that by using a res.once
function instead of res
in your runtime handler:
rest.get('https://api.github.com/users/:username', (req, res, ctx) => { - return res(ctx.status(500)) + return res.once(ctx.status(500)) })
In this tutorial, we have learned about the synergy between Storybook and Mock Service Worker, the benefits of granular control over mocked API responses when it comes to presenting the same component in multiple states, and how to integrate the two technologies together in a seamless manner.
Moreover, because MSW can run in both browser and Node.js, we can reuse the same API mocking logic for testing and development, concluding a fruitful and seamless integration.
You can find the source code of this example on GitHub and learn more about the API mocking in the MSW documentation.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowMatcha, a famous green tea, is known for its stress-reducing benefits. I wouldn’t claim that this tea necessarily inspired the […]
Backdrop and background have similar meanings, as they both refer to the area behind something. The main difference is that […]
AI tools like IBM API Connect and Postbot can streamline writing and executing API tests and guard against AI hallucinations or other complications.
Explore DOM manipulation patterns in JavaScript, such as choosing the right querySelector, caching elements, improving event handling, and more.