Angular Icon Get 67% off the Angular Master Bundle!

See the bundle then add to cart and your discount is applied.

0 days
00 hours
00 mins
00 secs
Nx

Mocking APIs for Storybook stories in an NX Monorepo

Storybook is easily one of my favorite tools in the world of UI/UX engineering. If you haven’t used it before, I highly recommend checking it out! This article will show you some Storybook but is not intended as a deep-dive.

Storybook gives us an easy-to-setup, easy-to-use framework for testing and demoing components in isolation and allowing us to simulate different component states and seeing the results without having to architect out the full app to get the component into that state. Super cool stuff.

NX is another favorite tool of mine. It is marketed as “a set of extensible tools for monorepos.” Monorepos are a very hot-topic right now in the engineering world and are gaining momentum, and NX with its CLI and toolchain make developing, structuring, and maintaining your monorepo a breeze (yet again, this article uses and will show nx and some usage with the CLI, but it is not aimed to be a monorepo or nx deep-dive).

If you prefer to check out the repo and follow along with that, the repo is here.

The problem at-hand

At my current role, we use both nx and storybook in a reasonably rapidly growing way in our architecture as we continue modernizing our stack and moving more and more of our UI/UX over to react. Storybook has been mega in allowing engineers to build and demo components to stakeholders without having to build up the full architecture into the full stack (which involves a Java/Spring API and a large amount of JSP).

All-in-all, this process, once established, has been pretty straight-forward and easy to implement, use and develop. But one problem that has come up is how to demo components that in some way or another make API requests and then interact with that data.

JavaScript Array Methods eBook Cover

🎉 Download it free!

Ready to go beyond ForEach? Get confident with advanced methods - Reduce, Find, Filter, Every, Some and Map.

  • Green Tick Icon Fully understand how to manage JavaScript Data Structures with immutable operations
  • Green Tick Icon 31 pages of deep-dive syntax, real-world examples, tips and tricks
  • Green Tick Icon Write cleaner and better-structured programming logic within 3 hours

As an extra bonus, we'll also send you some extra goodies across a few extra emails.

This was being solved by a mix of running the API while running storybook (very real-world, but also defeats the purpose a bit and involved a lot of, “Oh yeah, you gotta run the API and use an id that exists in your local db.”), pulling the API requests up a component and passing the data in to lower-level components as props (the container->view architecture approach, nothing wrong with this other than hooks and context make it less necessary), or not adding stories for these components (also very real-world, and you know sometimes you just gotta ship it).

All of these left me feeling a bit like, “There has got to be a way, right?” And of course, there is…

The solution: Mock-Service-Worker

When I finally got fed up enough to start searching for a solution, my thought process was along the lines of, “How do I mock the API requests being ran in storybook like what I would do in unit-testing?” Which lead me down a path of trying to figure out some like jest-esque way to mock our axios instance and then clear out those mocks, etc. etc. But I could not get any traction on it.

So, I opened up the googles and started going down the rabbit-hole with search terms like, “nx storybook api mock” with a few variations on the theme to try to find something positive (and you know, written somewhat recently; appreciate that after:{year} functionality). A combo of this and a suggestion from my friend Mat Warger lead me to mock-service-worker (msw for short, and the remainder of this article).

msw is an awesome lib that allows for the intercepting of network-level requests by providing mocks for these requests. These mocks are easily reused for testing, development, debugging, or, like what I needed, storybook development. It also has support for both rest and graphql, which is super cool as I have some lofty goals to use graphql with this app in the future; outside of being a huge graphql fan in-general.

The installation and setup are a breeze (as I will demo below) and the documentation is well laid out, easy to follow, and even has examples; honestly good documentation is becoming a must for me when I consider using a dep or library.

The only issue (and the crux of why I am writing this article) is that the msw, storybook nor the nx docs had any sort of examples of how I would combine these three frameworks to mock API requests for components in either apps or libs in my nx monorepo for my storybook stories. So I followed the advice tweeted out by Sam Julien, and decided to write a blog about how I went about solving this.

Getting Setup

This article assumes you have an existing (or will setup) an nx workspace that we will use for the duration of this article. This workspace should also have @nrwl/storybook configured; follow this tutorial to configure storybook in your workspace, if necessary. Our workspace will have these projects:

  • app:client - our react application and the default project in our workspace
  • lib:models - common, shareable models lib that other projects can use
  • lib:util-testing - test mocks and utils for our msw mocks and data mocking

Here is (a simplified) directory structure for our workspace.

|- .storybook/
|   |- main.js                                  <- this is the main configuration file for storybook since storybook v5.3+.
|   |- tsconfig.json
|   |- webpack.config.js                        <- webpack configuration for storybook in our workspace
|- apps/
|   |- client/
|     |- .storybook/                            <- this is the storybook configuration folder for our client project that was selected when we generated our configuration
|       |- main.js                              <- storybook configuration specific to our client project, extends the root storybook config
|       |- preview.js                           <- this used to be config.js. it configures the `preview` iframe that storybook uses to render the components
|       |- preview-head.html                    <- html that gets rendered into the iframe that displays the stories
|       |- tsconfig.json
|       |- webpack.config.js                    <- webpack configuration for client project
|     |- src/
|       |- app/
|         |- characters-list-rest/              <- directory for our Rick & Morty Characters list from the REST API
|           |- characters-list-rest.stories.tsx <- the stories for our characters-list-rest.tsx component
|           |- characters-list-rest.tsx         <- component that makes the request to get the characters list from the REST API and renders them
|         |- app.tsx                            <- app react component. has our routes and the app-frame
|         |- app.stories.tsx                    <- stories file for our app.tsx component
|       |- environments/
|         |- environment.prod.ts                <- environment values for production
|         |- environment.ts                     <- dev environment values
|       |- index.html                           <- app mount and entry point
|       |- main.tsx                             <- initializes our react app and renders it into the DOM at the mount point
|       |- theme.tsx                            <- builds our @material-ui theme and ThemeProvider for our app
|- libs/
|   |- models/                                  <- common lib containing our shared application models
|   |- util-testing/                            <- common lib contains utils and `msw` mocks for testing and storybook dev
|     |- src/
|       |- lib/mocks/
|         |- browser.ts                         <- sets up `msw` mock service worker with mock handlers
|         |- handlers.ts                        <- builds request mocks for `msw` to intercept API requests
|         |- rest.mocks.ts                      <- builds mock RestResponse data
|- public/
|   |- mockServiceWorker.js                     <- generated mock service worker by `msw`

By default (and configured in the apps/client/.storybook/main.js file), our storybook configuration looks for any files with a *.stories.{js|ts|jsx|tsx|mdx} naming convention (like app.stories.tsx) in any directory inside of apps/client/src.

Let’s start our storybook instance and see our stories!

# with npx
npx nx storybook client

# with nx-cli installed
nx storybook client

# or
nx run client:storybook

Once the startup completes successfully, our storybook instance will be running at http://localhost:4400 (yet again, configurable). Open a web browser and point to the url and we should see our app component stories. Another huzzah!

Storybook v6

With the release of storybook v6+, the storybook team also released an addon called @storybook/addon-essentials. This is an aggregation of many addons that have been prevalent and widely used in storybook. Addons like: knobs, actions, docs, accessibility, viewport, etc. With @storybook/addon-essentials, instead of having to install and config all of these separate addons, just install the essentials, and add to the addons in the root /.storybook/main.js export (and remove the addons) and we get access to use these essentials (as we will see when start working with stories).

# install addons-essentials
npm install --save-dev @storybook/addon-essentials

# with yarn
yarn add --dev @storybook/addon-essentials

# remove any other addons, @storybook/addon-knobs comes with the storybook generation from @nrwl/storybook
npm uninstall --save-dev @storybook/addon-knobs

# with yarn
yarn remove @storybook/addon-knobs

Open the root storybook config ./.storybook/main.js and add the @storybook/addon-essentials addon:

// ./.storybook/main.js
module.exports = {
  stories: [],
  addons: ['@storybook/addon-essentials'],
};

We also need to remove @storybook/addon-knob from the preview.js in our client storybook configuration. There is nothing else in this file, so it will now be empty. Don’t delete it as we will revist this file shortly.

Making API requests

Now that we have our repo, with a react app, and storybook setup, let’s make a component that makes an API request that we can then mock with msw. We will do a couple versions of this, one with rest and one with graphql to demo both and demo mocking both.

Rick and Morty API

This tutorial will utilize the free-to-use Rick & Morty API for both the rest and graphql API examples.

🕵️‍♂️ Check out the docs for the Rick & Morty API.

REST

Let’s update our characters-list-rest component to make the API request to the Rick & Morty API and render the data. Open the apps/client/src/app/characters-list-rest/characters-list-rest.tsx component file and let’s edit it to:

  1. Import the required components and models.
  2. Establish some component state for: loading, the characters list, and an error state just-in-case.
  3. Add a useEffect with the url prop as a dependency, that will make the rest request with the fetch api and update the state to reflect this request/handle errors.
  4. Return the JSX to render for the three-different states: loading, errors, with characters returned
// apps/client/src/app/characters-list-rest/characters-list-rest.tsx

// 1. imports
import React, { useState, useEffect } from 'react';
import { makeStyles, Theme } from '@material-ui/core/styles';
import Typography from '@material-ui/core/Typography';
import Card from '@material-ui/core/Card';
import CardHeader from '@material-ui/core/CardHeader';
import CardContent from '@material-ui/core/CardContent';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemAvatar from '@material-ui/core/ListItemAvatar';
import Avatar from '@material-ui/core/Avatar';
import ListItemText from '@material-ui/core/ListItemText';
import Skeleton from '@material-ui/lab/Skeleton';
import Alert from '@material-ui/lab/Alert';
import AlertTitle from '@material-ui/lab/AlertTitle';

// import our shared Character model from the `libs/models`
import { Character, RestResponse } from '@cmw-uc/models';

const useStyles = makeStyles((theme: Theme) => ({
  root: {
    width: '100%',
  },
  charTitle: {
    width: '100%',
    display: 'flex',
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
  },
  charStatus: {
    marginLeft: `${theme.spacing(2)}px`,
  },
}));

const CharactersListError: React.FC<{ errorMsg: string }> = ({ errorMsg }) => (
  <Alert severity="error" variant="filled">
    <AlertTitle>Rick & Morty REST API Character List Error</AlertTitle>
    <span>{errorMsg}</span>
  </Alert>
);

const CharacterListLoading = () => {
  const classes = useStyles();

  return (
    <div className={classes.root}>
      <Typography variant="h3" component="h3">
        <Skeleton />
      </Typography>
      <Skeleton variant="rect" width="100%" height={450} />
      <Skeleton variant="text" width="100%" />
      <Skeleton variant="text" width="100%" />
      <Skeleton variant="text" width="60%" />
    </div>
  );
};

const CharacterList: React.FC<{ characters: Character[] }> = ({ characters }) => {
  const classes = useStyles();

  return (
    <List className={classes.root}>
      {characters.map((char: Character) => (
        <ListItem key={char.id}>
          <ListItemAvatar>
            <Avatar alt={char.name} src={char.image} />
          </ListItemAvatar>
          <ListItemText
            primary={
              <Typography variant="subtitle1" className={classes.charTitle}>
                <span>{char.name}</span>
                <Typography variant="body2" className={classes.charStatus}>
                  {char.status}
                </Typography>
              </Typography>
            }
            secondary={
              <Typography variant="body1">
                <span className={classes.charInfoTitle}>Character info:</span>
                <span className={classes.charInfo}>{char.gender},</span>
                <span className={classes.charInfo}>{char.species},</span>
                <span className={classes.charInfo}>{char.origin.name},</span>
                <span className={classes.charInfo}>{char.location.name}</span>
              </Typography>
            }
          />
        </ListItem>
      ))}
    </List>
  );
};

export interface CharactersListRestProps {
  url: string;
}

const CharactersListRest: React.FC<CharactersListRestProps> = ({ url }) => {
  // 2. establish component state
  const classes = useStyles();
  const [loadingCharactersList, setLoadingCharactersList] = useState(false);
  const [characters, setCharacters] = useState<Character[]>([]);
  const [charactersListError, setCharactersListError] = useState<Error | null>(null);

  // 3. add the useEffect to retrieve Rick & Morty Character list from REST API
  // hit the Rick & Morty REST API and update the state
  useEffect(() => {
    setLoadingCharactersList(true);
    const retrieveCharactersList = async () => {
      try {
        const response: Response = await fetch(`${environment.rickAndMortyRestApiBaseUrl}/character`, {
          headers: {
            Accept: 'applications/json',
            'Content-Type': 'applications/json',
          },
        });
        if (response.status !== 200) {
          setCharactersListError(new Error('Did not successfully retrieve Rick & Morty Characters list'));
        } else {
          // get the response data as a RestResponse instance
          const data: RestResponse = await response.json();
          if (data == null) {
            setCharactersListError(new Error('Did not successfully retrieve Rick & Morty Characters list'));
          } else {
            setCharacters(data.results);
          }
        }
      } catch (error) {
        setCharactersListError(error);
      } finally {
        setLoadingCharactersList(false);
      }
    };

    retrieveCharactersList();
  }, [url]);

  // 4. Return JSX with components for different states
  return (
    <section className={classes.root}>
      <Card>
        <CardHeader title="Rick & Morty REST API Characters List" />
        <CardContent>
          {loadingCharactersList ? <CharacterListLoading /> : null}
          {charactersListError != null ? <CharactersListError errorMsg={charactersListError.message} /> : null}
          {!loadingCharactersList && charactersListError == null ? <CharacterList characters={characters} /> : null}
        </CardContent>
      </Card>
    </section>
  );
};

export default CharactersListRest;

And here is our component that loads the characters and renders them into the DOM as well as some loading and error states. It takes a url: string component prop that is used to make the API request; this is so we can pass in different URLs to mock.

Character List REST Component Stories

Let’s create storybook file for our characters-list-rest component to demo the component in isolation.

Optional If you decided to use @material-ui, as I did, let’s real quick configure our stories to use our theme and fonts.

Create a preview-head.html file in our apps/client/.storybook directory. Whatever we put in this html will get added to the head of our iframe that renders our stories.

<!-- apps/client/.storybook/preview-head.html -->
<link rel="preconnect" href="https://fonts.gstatic.com" />
<link href="https://fonts.googleapis.com/css2?family=Open+Sans+Condensed:[email protected]&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />

And now update the preview.js to wrap all of our stories in our ThemeProvider.

// apps/client/.storybook/preview.js
import React from 'react';
import { addDecorator } from '@storybook/react';
// my custom material-ui ThemeProvider/theme
import { AppThemeProvider } from '../src/theme';

// wrap all of our stories in our AppThemeProvider
addDecorator((story) => <AppThemeProvider>{story()}</AppThemeProvider>);

Our stories file will be pretty light as the characters-list-rest component doesn’t do a whole lot and the three states are determined by the interaction with the fetch API. Let’s take a look.

// apps/client/src/app/characters-list-rest/characters-list-rest.stories.tsx
import React from 'react';

import { environment } from '../../environments/environment';
import CharactersListRest, { CharactersListRestProps } from './characters-list-rest';

const characterUrls = {
  default: `${environment.rickAndMortyRestApiBaseUrl}/character`,
  largeList: `${environment.rickAndMortyRestApiBaseUrl}/character/large-list`,
  smallList: `${environment.rickAndMortyRestApiBaseUrl}/character/small-list`,
  error: `${environment.rickAndMortyRestApiBaseUrl}/character/error`,
};

export default {
  component: CharactersListRest,
  title: 'Components / Characters List / REST',
  argTypes: {
    url: {
      control: {
        type: 'select',
        options: {
          'Default (Passthrough)': characterUrls.default,
          'Large Character List': characterUrls.largeList,
          'Small Character List': characterUrls.smallList,
          Error: characterUrls.error,
        },
      },
    },
  },
};

const Template = (args: CharactersListRestProps) => <CharactersListRest {...args} />;

export const CharactersListRestDemo = Template.bind({});
CharactersListRestDemo.args = {
  url: characterUrls.default,
};

This stories structure is called Component Story Format (CSF) and follows the v6 strategy of building stories for components. Defining the argTypes on our default export allows us to change the value of different components props (in this case just url) in our story via the controls and see the different results. If we run our storbook for client again, we can view the Characters List Rest Demo and see our component as well as our App component stories.

JavaScript Array Methods eBook Cover

🎉 Download it free!

Ready to go beyond ForEach? Get confident with advanced methods - Reduce, Find, Filter, Every, Some and Map.

  • Green Tick Icon Fully understand how to manage JavaScript Data Structures with immutable operations
  • Green Tick Icon 31 pages of deep-dive syntax, real-world examples, tips and tricks
  • Green Tick Icon Write cleaner and better-structured programming logic within 3 hours

As an extra bonus, we'll also send you some extra goodies across a few extra emails.

As you will notice, our stories still make and show the data returned from the Rick & Morty API here as well. In this case because this API is public, this is fine. But the point of stories is to view components in isolation, meaning without outside involvement; including API requests.

Time for msw.

Adding msw

The time has finally come. The crux of the article. It is time to add and configure msw to our workspace and mock our API requests. The msw docs do a fantastic job of walking you through the install and set up, so I will summarize here:

Install msw as a dev dependency

# with npm
npm install --save-dev msw

# with yarn
yarn add msw

Now we need to define our mock handlers. Open/create the libs/util-testing/src/lib/mocks/handlers.ts file and define our mocks to return our mocked data or state based off the url:

// libs/util-testing/src/lib/mocks/handlers.ts
import { RestResponse } from '@cmw-uc/models';
import { rest, RequestHandler } from 'msw';

import { buildRestResponse } from './rest.mocks';

const smallCharCountResponse = buildRestResponse(5);
const largeCharCountResponse = buildRestResponse(20);

export const handlers: RequestHandler[] = [
  // mock the character list endpoint
  rest.get('https://rickandmortyapi.com/api/character/:state', (req, res, ctx) => {
    // check the `state` request param to determine what to return for our mocked response
    // the values of state are determined in our `characters-list-rest.stories.tsx`
    const { state } = req.params;
    switch (state) {
      case 'small-list': {
        // return a successful response after a 1.5sec delay to show our loading indicator
        return res(ctx.status(200), ctx.delay(1500), ctx.json(smallCharCountResponse));
      }
      case 'error': {
        // return an error response of 404: Not Found
        return res(ctx.status(404), ctx.delay(1500), ctx.json({ errorMessage: 'Failure' }));
      }
      default: {
        // return a successful response after a 1.5sec delay to show our loading indicator
        return res(ctx.status(200), ctx.delay(1500), ctx.json(largeCharCountResponse));
      }
    }
  }),
];

The meat of this is in the handlers. This defines a list of rest request handlers for the API endpoint. Notice the :state URL param. We introspect this value and based off the value, return a different response. These values are supplied and defined in our apps/client/src/app/characters-list-rest/characters-list-rest.stories.tsx story file for different mock API endpoint options to demo. This is a really handy abstraction for intercepting the API requests and returning different states for the response for our component to then use and render different component states, such as error, a short list, a long list, and to see our loading skeleton.

For our client storybook instance to know about these mocks we need to setup a service worker and pass in our handlers. Create a file in the libs/util-testing/src/lib/mocks directory called browser.ts (we use browser as stories run in a browser, as opposed to server if this was to mock on a node server).

// lib/util-testing/src/lib/mocks/browser.ts
import { setupWorker, SetupWorkerApi } from 'msw';

import { handlers } from './handlers';

// this configures a browser service work with our mocked request handlers
export const worker: SetupWorkerApi = setupWorker(...handlers);

We also need to create a mock service worker file that our storybook instance will use. msw will generate this file for us, we just need to point it to the appropriate directory. According to the docs this should be a public directory. This is interesting as in nx we don’t have a public directory like you would in a react app, etc. Note there may be a more appropriate way to do this, but this is how I got this to work. Create a directory at the root workspace level named public:

# open a terminal in the root workspace directory
mkdir public

Now run the msw init command

# open a terminal in the root workspace directory
npx msw init public/

This will create a file: public/mockServiceWorker.js.

We now need to import the exported worker from our libs/util-testing/src/lib/mocks/browser.ts and register it when we start storybook in our client. Open the apps/client/.storybook/preview.js, import and register our worker.

// apps/client/.storybook/preview.js

// Storybook executes this module in both bootstap phase (Node)
// and a story's runtime (browser). However, cannot call `setupWorker`
// in Node environment, so need to check if we're in a browser.
if (typeof global.process === 'undefined') {
  // note: we must use relative imports here
  const { worker } = require('../../../libs/util-testing/src/lib/mocks/browser');
  // Start the mocking when each story is loaded.
  // Repetitive calls to the `.start()` method do not register a new worker,
  // but check whether there's an existing once, reusing it, if so.
  worker.start();
}

Note When I did this the first time, I was inclined to export the worker out through the util-testing lib like I would with any lib and then import it like this: const { worker } = require('@cmw-uc/util-testing');. While this works when you run storybook, the issue is, it will fail when you run your test suite. This is because the test suite is run in a node environment. Because of that, when the @cmw-uc/util-testing is imported, it will register the worker with the setupWorker which will then fail as setupWorker is intended for a browser environment. So while admittedly a bit gnarly, the way I solved this issue was by using a relative import instead of the pathed import from @cmw-uc/util-testing.

Now, let’s start our client storybook instance again with our mocks. For storybook to pick up and register our mockServiceWorker, we need to point it at our public static directory. We can do this when we run the cli command to start storybook by passing in the --static-dir config paramter with a value of public

nx storybook client --static-dir=public

And open our storybook in the browser at http://localhost:4400, open the browser console and you should see a console.log line that says [MSW] Mocking enabled; this is the indication that our service worker mocking is successful and we can mock our API requests and handle them with different resulsts and error states! As we change the url value in the dropdowns to the different options, our story should show the different results of the different list lengths and error state or passthrough to the actual API.

This is mega cool and very helpful for taking our storybook and app development up in our nx monorepo.

Note We can also permanently set the --static-dir=public storybook cli param by opening our workspakce.json file and updating the storybook configuration for our client by adding a "staticDir": ["public"] option to the configuration object.

{
  "version": 1,
  "projects": {
    "client": {
      "root": "apps/client",
      "sourceRoot": "apps/client/src",
      "projectType": "application",
      "schematics": {},
      "architect": {
        // ... other client architecture configuration
        "storybook": {
          "builder": "@nrwl/storybook:storybook",
          "options": {
            "uiFramework": "@storybook/react",
            "port": 4400,
            "config": {
              "configFolder": "apps/client/.storybook"
            },
            "staticDir": ["public"] // add here for running storybook
          },
          "configurations": {
            "ci": {
              "quiet": true
            }
          }
        },
        "build-storybook": {
          "builder": "@nrwl/storybook:build",
          "options": {
            "uiFramework": "@storybook/react",
            "outputPath": "dist/storybook/client",
            "config": {
              "configFolder": "apps/client/.storybook"
            },
            "staticDir": ["public"] // add here for build-storybook
          },
          "configurations": {
            "ci": {
              "quiet": true
            }
          }
        }
      }
    }
    // ... other projects
  }
  // ... remainder of workspace json
}

Conclusion

nx and storybook are very useful and powerful toolsets to architect and build enterprise-grade applications. Adding in msw gives us the ability to take that a step-further but not letting API requests determine our architecture decisions y allowing us to mock those requests and see the results of those mocks in our stories and view our components in isolation with controlled conditions.

Thank you for reading along. I hope you found this article helpful.

Check out my other UltimateCourses blogs and follow me on Twitter @cmwhited.

References