TypeSpec、Orval、SWR、MSWによる自動生成APIモックを活用したNext.js + Storybook開発

2024年7月9日
TypeSpec、Orval、SWR、MSWによる自動生成APIモックを活用したNext.js + Storybook開発

前回、TypeSpecを使ってOpenAPI定義を作成し、Orvalを使ってOpenAPI定義から、SWRのAPIフックを自動生成した。

今回は、その自動生成したAPIフックのMSW(Mock Service Worker)のモック機能を活用し、 StorybookからMSWのモックAPIに対してリクエストを行う方法をまとめる。

MSWとは

MSW(Mock Service Worker)はWEBアプリケーション向けのモックライブラリで、 これを使うとバックエンドAPIをモックしてフロントエンドの開発を進めることができる。

MSWはブラウザのService Workerを使用し、ブラウザ内でAPIへのリクエストをインターセプトしてモックレスポンスを返すことができる。

他の方法に比べて、以下の利点がある。

  • コード内でモックに差し替える方法に比べると、モック用の追加の実装を用意する必要がないので、コードがシンプルになる
  • JSON Serverなどの方法に比べると、フロントサーバーと別プロセスでモックサーバーを立てたりする必要がなく、環境をシンプルに保てる

なお、サーバーサイド(Node.js環境)でも同じように動作させられ、 その場合、Service Workerの代わりにNode.jsのhttpモジュールをインターセプトする。

Storybook

Storybookを使うと、UIコンポーネントを独立して開発することができる。

Next.js 13では、MSWと相性が悪いようで、実際にNext.jsアプリケーションを立ち上げて、モックデータを取得することができなかった。(以下のIssueのコメントよると、Pages routerでもApp routerでも動作しないとのこと)

今回のプロジェクトでは、Storybookをベースにコンポーネントを作成しており、 Storybook上でMSWを利用できれば十分だったので、StorybookからMSWを使う方法をまとめる。

Storybookへの追加

msw用のアドオンが利用できる。

以下のコマンドでインストールする。

npm i msw msw-storybook-addon -D

Service workerの作成

Service Worker用のワーカースクリプトを動作させるために、ワーカースクリプトをpublic/ディレクトリに追加する。

npx msw init public/

.storybook/preview.tsの修正

以下のアドオン設定を追加する。 handlersは、この後作成する。

+ import { initialize, mswLoader } from "msw-storybook-addon";
+ import { handlers } from "../mocks/handlers";

+ // Initialize MSW
+ initialize();

const preview: Preview = {
  parameters: {
+   msw: {
+     handlers
+   }
  },
+ loaders: [mswLoader]
};

OrvalでMSWモックを生成

orval.config.tsへ追記

前回の記事の設定に、mock: trueを追加して、 SWRファイルの作成と一緒にモックも作成するように変更する。

module.exports = {
  "user-api": {
    input: {
      target: "./openapi.yml",
    },
    output: {
      mode: "split",
      target: "./api/endpoints",
      schemas: './api/models',
      client: "swr",
+     mock: true,
    },
    hooks: {
      afterAllFilesWrite: "prettier --write",
    },
  },
};

userAPI.msw.tsの生成

モックの生成を有効化した後、npx orvalを実行すると、以下のuserAPI.msw.tsが生成される。 mswのモック定義が自動的に生成されていることがわかる。 モックデータは、@faker-js/fakerを使って、ランダムなデータが生成している。

/**
 * Generated by orval v6.31.0 🍺
 * Do not edit manually.
 * User API
 * OpenAPI spec version: 0.0.0
 */
import {
  faker
} from '@faker-js/faker'
import {
  HttpResponse,
  delay,
  http
} from 'msw'
import type {
  User
} from '../models'

export const getUsersGetUsersResponseMock = (): User[] => (Array.from({ length: faker.number.int({ min: 1, max: 10 }) }, (_, i) => i + 1).map(() => ({email: faker.word.sample(), id: faker.word.sample(), name: faker.word.sample()})))

export const getUsersGetUserResponseMock = (overrideResponse: Partial< User > = {}): User => ({email: faker.word.sample(), id: faker.word.sample(), name: faker.word.sample(), ...overrideResponse})


export const getUsersGetUsersMockHandler = (overrideResponse?: User[] | ((info: Parameters<Parameters<typeof http.get>[1]>[0]) => Promise<User[]> | User[])) => {
  return http.get('*/users', async (info) => {await delay(1000);
    return new HttpResponse(JSON.stringify(overrideResponse !== undefined 
            ? (typeof overrideResponse === "function" ? await overrideResponse(info) : overrideResponse) 
            : getUsersGetUsersResponseMock()),
      {
        status: 200,
        headers: {
          'Content-Type': 'application/json',
        }
      }
    )
  })
}

export const getUsersGetUserMockHandler = (overrideResponse?: User | ((info: Parameters<Parameters<typeof http.get>[1]>[0]) => Promise<User> | User)) => {
  return http.get('*/users/:id', async (info) => {await delay(1000);
    return new HttpResponse(JSON.stringify(overrideResponse !== undefined 
            ? (typeof overrideResponse === "function" ? await overrideResponse(info) : overrideResponse) 
            : getUsersGetUserResponseMock()),
      {
        status: 200,
        headers: {
          'Content-Type': 'application/json',
        }
      }
    )
  })
}
export const getUserAPIMock = () => [
  getUsersGetUsersMockHandler(),
  getUsersGetUserMockHandler()
]

mocks/handlers.tsの追加

mocksディレクトリを作成する。

mkdir mocks
cd mocks

先ほど作成したgetUserAPIMockを使って、mocks/handlers.tsにモックAPIのハンドラを追加する。

+ import { getUserAPIMock } from "../api/userAPI.msw.ts";
+
+ export const handlers = getUserAPIMock();

このhanlders.storybook/preview.tsのmswのhandlersの設定に渡され、 Storybook上でMSWのモックAPIを利用できるようになる。

コンポーネントの利用方法は、前回と同様、生成されたSWRのカスタムフックをReactコンポーネントで呼び出せば良い。 内部的には、MSWのモックAPIが呼び出される。

const { data, error, isLoading } = useUsersGetUsers();

モックデータをカスタムしたい場合

stringで定義した場合、faker.word.sample() モックをカスタムしたい場合は、以下のようにpropertiesオプションでカスタムすることができる。

https://orval.dev/reference/configuration/output#mock

たとえば、名前、Email、画像URLなどをランダムなデータにしたい場合は、以下のように設定する。

module.exports = {
  "user-api": {
    input: {
      target: "./openapi.yml",
    },
    output: {
      mode: "split",
      target: "./api/endpoints",
      schemas: './api/models',
      client: "swr",
      mock: true,
+     override: {
+       mock: {
+         properties: {
+           name: () => faker.name.fullName(), // nameフィールドはランダムな名前を返すように変更
+           email: () => faker.internet.email(), // emailフィールドはランダムなメールアドレスを返すように変更
+           '/image/': () => faker.image.url(), // imageとつくフィールドは画像URLを返すように変更
+         },
+       }
+     },
      hooks: {
        afterAllFilesWrite: "prettier --write",
      },
    },
  },
};

まとめ

最終的に開発の流れは以下の通りとなる。

  1. TypeSpecでAPI定義をtspファイルで作成
  2. tspファイルからOpenAPIのyamlファイルを自動生成
  3. Orvalを使って、OpenAPIのyamlファイルからSWRフックとMSWのモック実装を自動生成
  4. APIのMSWモックを使って、Storybook上でコンポーネントの開発を進める