Mocking API Calls
🧠

Mocking API Calls

Summary

Using MSW + MSW Storybook Addon, we are able to proxy any API calls to a Service Worker that's located in the ./public folder. Below we will go into the steps suggested for setting up a story that requires data from multiple different types of APIs and how they're each mocked with MSW.

We will take a heavier focus on REST APIs, simply because these are most likely what a majority of developers would be asked to mock - but if this does not interest you, feel free to check out the GraphQL guide at the bottom!

Mock Data

In an ideal world, the teams responsible for the endpoints you are consuming will have easily accessible data contracts / mock data. If this is not the case, try to identify successful responses from each of your endpoints to curate your own. Storing mocked data separate from your routes makes the data integrity easier to maintain over time and at scale. It's significantly easier to import the data to your mocked routes than it is to scroll through a seemingly infinite sea of JSON.

// data.js

export const FilmOneMockData = {
  // data pertaining to Film One here
}

export const FilmTwoMockData = {
  // data pertaining to Film Two here
}
Example of exporting mocked data for sake of file hierarchy organization
// script.js

import { FilmOneMockData, FilmTwoMockData } from "./data"

// consume mocked data here
Example of importing mocked data for consumption

REST

Let's compare an Express route to an MSW route to highlight similarities between what might already be familiar to most developers. Keeping things as simple as possible, we will start with a route that returns a payload { message: "hello world" } when a GET request is sent to the route "/helloWorld" .

// Express GET Route
const express = require('express')
const app = express()

app.get('/helloWorld', function (req, res) {
  res.json({ message: 'hello world' })
})
https://expressjs.com/
// MSW GET Route
import { rest } from "msw";


rest.get('/helloWorld', (req, res, ctx) => {
  return res(ctx.json({ message: 'hello world' }))
})
https://mswjs.io/

Both tools call a similarly composed function to api.get(route, handler) , where the first argument passed is the route associated with the GET request and the second argument is the handler function where we can determine the logic associated with this route.

MSW's context ctx is an argument of a response resolver that contains a set of response transformers to compose a mocked response. You can find these transformers listed in the side bar under context here, where we link you to the most commonly used ctx.json()

Mocking Success

// MSW GET Route - Success
import { rest } from "msw";


rest.get('/helloWorld', (req, res, ctx) => {
  return res(ctx.json({ message: 'hello world' }))
})
https://mswjs.io/

Mocking Error

You can change the HTTP error code passed to ctx.status() to mock particular error scenarios, as well as modify the delay of response (in milliseconds) by modifying the value passed to ctx.delay() .

// MSW GET Route - Error
import { rest } from "msw";


rest.get('/helloWorld', (req, res, ctx) => {
  return res(ctx.json(ctx.delay(800), ctx.status(403)))
})
https://mswjs.io/

Grouping Routes Together

Below, we cover how to compose arrays of routes so that we can bundle them together for consumption. The largest benefit to this is the ability to recreate specific scenarios within your application/component where the endpoints it consumes may be in varying states of up or down.

import { rest } from "msw";
import { AuthMockData, CheckoutMockData } from "./data"

const MockedRoutes = [
	rest.get("/api/auth", (req, res, ctx) => {
	    return res(ctx.json(AuthMockData));
	}),
  rest.get("/api/checkout", (req, res, ctx) => {
	    return res(ctx.json(CheckoutMockData));
  })
]
https://mswjs.io

Mocking with mixed Success/Error

This will enable us to mock cases where only SOME services/endpoints are down so that we can determine how to build our frontends with high quality error handling

Let's use an ecommerce store as an example, where our two consumed routes are /api/auth and /api/checkout . Users of this store will not be able to check out with their account associated details if the /api/auth route is failing and may have to checkout as a guest, if supported by the store. It would also not be possible for the users of this store to checkout if the /api/checkout route is failing.

Possible route success/failure states

/api/auth ✅ & /api/checkout
/api/auth ✅ & /api/checkout
/api/auth ❌ & /api/checkout
/api/auth ❌ & /api/checkout

Consuming MSW Routes in a Storybook Story

Using the scenarios from the previous example, let's take a closer look at what consumption of our newly mocked mixed Success/Error routes would look like.

// ./routes.js

import { rest } from "msw";
import { AuthMockData, CheckoutMockData } from "./data";

const MockedSuccessRoutes = [
	rest.get("/api/auth", (req, res, ctx) => {
	    return res(ctx.json(AuthMockData));
	}),
  rest.get("/api/checkout", (req, res, ctx) => {
	    return res(ctx.json(CheckoutMockData));
  })
];

const MockedErrorRoutes = [
	rest.get("/api/auth", (req, res, ctx) => {
	    return res(ctx.delay(800), ctx.status(403)));
	}),
  rest.get("/api/checkout", (req, res, ctx) => {
	    return res(ctx.delay(800), ctx.status(403)));
  })
];

const MockedOnlyAuthUpRoutes = [
	rest.get("/api/auth", (req, res, ctx) => {
	    return res(ctx.json(AuthMockData));
	}),
  rest.get("/api/checkout", (req, res, ctx) => {
	    return res(ctx.delay(800), ctx.status(403)));
  })
];

const MockedOnlyCheckoutUpRoutes = [
	rest.get("/api/auth", (req, res, ctx) => {
	    return res(ctx.delay(800), ctx.status(403)));
	}),
  rest.get("/api/checkout", (req, res, ctx) => {
	    return res(ctx.json(CheckoutMockData));
  })
];
https://mswjs.io
// ./App.stories.js

import React from "react";
import { App } from "./App";
import {
	MockedSuccessRoutes,
	MockedErrorRoutes,
	MockedOnlyAuthRoutes,
	MockedOnlyCheckoutRoutes 
} from "./routes";

export default {
  title: `App w/ Mocked Routes`,
  component: App,
};

export const DefaultBehavior = () => <App />;

const MockTemplate = () => <App />;

export const MockedSuccess = MockTemplate.bind({});
MockedSuccess.parameters = {
  msw: MockedSuccessRoutes,
};

export const MockedError = MockTemplate.bind({});
MockedError.parameters = {
  msw: MockedErrorRoutes,
};

export const MockedOnlyAuth = MockTemplate.bind({});
MockedOnlyAuth .parameters = {
  msw: MockedOnlyAuthRoutes,
};

export const MockedOnlyCheckout = MockTemplate.bind({});
MockedOnlyCheckout.parameters = {
  msw: MockedOnlyCheckoutRoutes,
};

GraphQL (Coming Soon)