<

How to Mock tRPC Communication on Storybook with MSW

Introduction

tRPC is a framework that allows you to easily build type-safe APIs. During development, if you want to proceed with front-end development on Storybook without waiting for the backend implementation, you can mock the API using Mock Service Worker (MSW). In this article, we will explain how to mock tRPC communication with MSW using maloguertin/msw-trpc. As a practical example, the sample code is shared on the GitHub repository silverbirder/trpc-msw-storybook-nextjs.

Technology Stack

First, let's introduce the libraries we will use. The following dependencies are listed in package.json. (Only a part is extracted)

{
  "dependencies": {
    "@tanstack/react-query": "^4.36.1",
    "@trpc/client": "^10.45.1",
    "@trpc/next": "^10.45.1",
    "@trpc/react-query": "^10.45.1",
    "@trpc/server": "^10.45.1",
    "next": "^14.1.0",
    "react": "18.2.0"
  },
  "devDependencies": {
    "@storybook/nextjs": "^7.6.17",
    "@storybook/react": "^7.6.17",
    "msw": "^2.2.2",
    "msw-storybook-addon": "^2.0.0--canary.122.b3ed3b1.0",
    "msw-trpc": "^2.0.0-beta.0",
    "storybook": "^7.6.17"
  }
}

Preparation

To create a project template, execute the following command.

npm create t3-app@latest
npx storybook@latest init 
npm i msw
npx msw init ./public --save

This will set up the basic integration of tRPC and MSW.

Sample Component

Next, the component used as a sample is as follows. It is a component generated by t3-app, with some modifications.

// ~/app/_components/create-post.tsx
"use client";

import { useState } from "react";

import { api } from "~/trpc/react";

export function CreatePost() {
  const [name, setName] = useState("");
  const [postName, setPostName] = useState("");

  const createPost = api.post.create.useMutation({
    onSuccess: ({name}) => {
      setName("");
      setPostName(name);
    },
  });

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        createPost.mutate({ name });
      }}
      className="flex flex-col gap-2"
    >
      <input
        type="text"
        placeholder="Title"
        value={name}
        onChange={(e) => setName(e.target.value)}
        className="w-full rounded-full px-4 py-2 text-black"
      />
      <button
        type="submit"
        className="rounded-full bg-white/10 px-10 py-3 font-semibold transition hover:bg-white/20"
        disabled={createPost.isLoading}
      >
        {createPost.isLoading ? "Submitting..." : "Submit"}
      </button>
      <div>{postName}</div>
    </form>
  );
}

We are using api.post.create.useMutation to send data via tRPC.

Integration of MSW and tRPC

To integrate MSW and tRPC, first install the maloguertin/msw-trpc package.

npm i msw-trpc --save-dev

Then, place the MSW and tRPC integration code in ~/trpc/msw.tsx and write it as follows.

// ~/trpc/msw.tsx
"use client";

import { useState } from "react";
import { createTRPCMsw } from "msw-trpc";
import { httpLink } from "@trpc/client";
import { createTRPCReact } from "@trpc/react-query";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { getUrl, transformer } from "./shared";
import type { AppRouter } from "~/server/api/root";

export const trpcMsw = createTRPCMsw<AppRouter>({
  baseUrl: getUrl(),
  transformer: { input: transformer, output: transformer },
});

export const api = createTRPCReact<AppRouter>();

export const TRPCReactProvider = (props: { children: React.ReactNode }) => {
  const [queryClient] = useState(() => new QueryClient());
  const [trpcClient] = useState(() =>
    api.createClient({
      transformer,
      links: [
        httpLink({
          url: getUrl(),
          headers() {
            return {
              "content-type": "application/json",
            };
          },
        }),
      ],
    }),
  );

  return (
    <QueryClientProvider client={queryClient}>
      <api.Provider client={trpcClient} queryClient={queryClient}>
        {props.children}
      </api.Provider>
    </QueryClientProvider>
  );
};

trpcMsw is used in the MSW handler. For more details, please check createTRPCMsw in maloguertin/msw-trpc. I was in trouble because I didn't understand the arguments of createTRPCMsw at first. TRPCReactProvider is the MSW version of the tRPC provider.

Integration of MSW and Storybook

To run MSW in Storybook, use mswjs/msw-storybook-addon. To install, execute the following command.

npm i msw-storybook-addon --save-dev

※ For various reasons, we specify msw-storybook-addon@2.0.0--canary.122.b3ed3b1.0.

To set up the display in Storybook, configure .storybook/preview.tsx as follows.

// /.storybook/preview.tsx
import React from "react";
import type { Preview } from "@storybook/react";
import { initialize, mswLoader } from "msw-storybook-addon";
import { TRPCReactProvider } from "../src/trpc/msw";
initialize();

const preview: Preview = {
  parameters: {
    actions: { argTypesRegex: "^on[A-Z].*" },
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/i,
      },
    },
  },
  loaders: [mswLoader],
  decorators: [
    (Story) => (
      <TRPCReactProvider>
        <Story />
      </TRPCReactProvider>
    ),
  ],
};

export default preview;

Wrap decorators with TRPCReactProvider. This allows you to mock tRPC communication in Storybook with MSW.

Storybook's stories file

Finally, the Storybook's stories file is defined as follows.

// ~/app/_components/create-post.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';

import { CreatePost } from './create-post';
import { trpcMsw } from '~/trpc/msw';

const meta = {
  title: 'create-post',
  component: CreatePost,
  parameters: {
    layout: 'centered',
  },
  tags: ['autodocs'],
  argTypes: {
    backgroundColor: { control: 'color' },
  },
} satisfies Meta<typeof CreatePost>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Main: Story = {
  parameters: {
    msw: {
      handlers: [
        trpcMsw.post.create.mutation(({name}) => {
          const post = { id: 1, name: name };
          return post;
        })
      ],
    },
  }
};

In parameters.switch.handlers, we are mocking the tRPC's mutation with MSW. Now, when you run npm run storybook, you can check the UI with the mocked tRPC.

In conclusion

I hope this article has been helpful to you.

If it was helpful, support me with a ☕!

Share

Related tags