Artem Zakharchenko Full-stack software engineer, medical doctor, musician.

Using Storybook and Mock Service Worker for mocked API responses

7 min read 2233

Storybook and Mock Service Worker API Components

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.

What is Mock Service Worker?

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!


Setting up a Storybook and Mock Service Worker project

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.

Installing Storybook

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

Initializing Service Worker

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.

Creating a React Component

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.

Writing a story

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.

Implementing API mocking

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.

Declaring request handlers

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.

MSW and API integration

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, as preview.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:

GitHubUser Component on Storybook
Storybook page showcasing our “GitHubUser” component receiving the mocked response.

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.

Per-story API responses

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.

Mocking a loading state

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.

GitHubUser component on Storybook in Loading State
Storybook page showcasing our “GitHubUser” component in a loading state.

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.

Mocking error responses

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:

GitHubUser Component on Storybook Experiencing an Error
Storybook page showcasing our “GitHubUser” component’s behavior when it receives an error.

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))
})

Conclusion

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.

Get set up with LogRocket's modern error tracking in minutes:

  1. Visit https://logrocket.com/signup/ to get an app ID
  2. Install LogRocket via npm or script tag. LogRocket.init() must be called client-side, not server-side
  3. $ 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>
  4. (Optional) Install plugins for deeper integrations with your stack:
    • Redux middleware
    • NgRx middleware
    • Vuex plugin
Get started now
Artem Zakharchenko Full-stack software engineer, medical doctor, musician.

Leave a Reply