🐟

TypeSpec、Orval、Storybook を使ってフロントエンドのモック生成を自動化する

2024/07/17に公開2

はじめに

フロントエンド開発において、効率的かつ一貫性のあるモック生成は非常に重要です。本記事では TypeSpec、Orval、Storybook の 3 つのツールを使用して自動生成でモックを実現する方法を紹介します。

TypeSpec は、大規模な API を提供するために Microsoft が開発し、使用している新しい API 記述言語です。
https://typespec.io/

Orval は、OpenAPI 仕様から TypeScript のクライアントコードを生成するツールです。これにより、最新の API 仕様に基づいたクライアントコードを常に保持し、API との通信がスムーズに行えるようになります。
https://orval.dev/

Storybook は、コンポーネントを独立して開発・テストするためのインタラクティブなツールです。コンポーネントの見た目や動作を個別に確認できるため、UI の一貫性を保ちながら効率的に開発を進めることができます。
https://storybook.js.org/

TypeSpec を使用して OpenAPI ファイルを生成する

セットアップ

以下を参考に、TypeSpec のインストール、拡張機能のダウンロードなどをします。

https://typespec.io/docs#install-tsp

出力先が気になったので generated ディレクトリの直下に出力されるように変更してみました。

tspconfig.yaml
output-dir: "{cwd}/generated"
options:
  "@typespec/openapi3":
    emitter-output-dir: "{output-dir}/"
emit:
  - "@typespec/openapi3"

TypeSpec への記述と OpenAPI ファイルの生成

TypeSpec のREADME記載の tsp ファイル(PetStore)を拝借して OpenAPI ファイルの生成を試してみます。

main.tsp
import "@typespec/http";
import "@typespec/rest";
import "@typespec/openapi3";

using TypeSpec.Http;
using TypeSpec.Rest;

/** This is a pet store service. */
@service({
  title: "Pet Store Service",
})
@server("https://example.com", "The service endpoint")
namespace PetStore;

@route("/pets")
interface Pets {
  list(): Pet[];
}

model Pet {
  @minLength(100)
  name: string;

  @minValue(0)
  @maxValue(100)
  age: int32;

  kind: "dog" | "cat" | "fish";
}

コンパイルすると OpenAPI ファイルが生成されました。

generated/openapi.yaml
openapi: 3.0.0
info:
  title: Pet Store Service
  description: This is a pet store service.
  version: 0.0.0
tags: []
paths:
  /pets:
    get:
      operationId: Pets_list
      parameters: []
      responses:
        '200':
          description: The request has succeeded.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Pet'
components:
  schemas:
    Pet:
      type: object
      required:
        - name
        - age
        - kind
      properties:
        name:
          type: string
          minLength: 100
        age:
          type: integer
          format: int32
          minimum: 0
          maximum: 100
        kind:
          type: string
          enum:
            - dog
            - cat
            - fish
servers:
  - url: https://example.com
    description: The service endpoint
    variables: {}

Orval を使用して型定義・モック・クライアントコードを生成する

インストール

npm i orval -D

設定ファイルの作成

orval.config.cjs を作成し、簡単に設定を記述します。

orval.config.cjs
module.exports = {
  "petstore-file": {
    input: "./generated/openapi.yaml",
    output: "./src/petstore.ts",
  },
};

orval --config ./orval.config.cjsを実行してみると型定義(PetKind, Pet)とクライアントコード(petsList)が生成されました。

petstore.ts
/**
 * Generated by orval v6.31.0 🍺
 * Do not edit manually.
 * Pet Store Service
 * This is a pet store service.
 * OpenAPI spec version: 0.0.0
 */
import * as axios from 'axios';
import type {
  AxiosRequestConfig,
  AxiosResponse
} from 'axios'
export type PetKind = typeof PetKind[keyof typeof PetKind];


// eslint-disable-next-line @typescript-eslint/no-redeclare
export const PetKind = {
  dog: 'dog',
  cat: 'cat',
  fish: 'fish',
} as const;

export interface Pet {
  /**
   * @minimum 0
   * @maximum 100
   */
  age: number;
  kind: PetKind;
  /** @minLength 100 */
  name: string;
}

  export const petsList = <TData = AxiosResponse<Pet[]>>(
     options?: AxiosRequestConfig
 ): Promise<TData> => {
    return axios.default.get(
      `/pets`,options
    );
  }

export type PetsListResult = AxiosResponse<Pet[]>

モックが生成されるようにする

設定ファイル(orval.config.cjs)を修正して、モックを生成するようにします。

orval.config.cjs
module.exports = {
  "petstore-file": {
    input: "./generated/openapi.yaml",
    output: "./src/petstore.ts",
  },
};

すると faker-js でモックデータを生成する関数(getPetsListResponseMock)と、msw でモックをハンドルする関数(getPetsListMockHandler)が生成されました 🎉

petstore.ts
/**
 * Generated by orval v6.31.0 🍺
 * Do not edit manually.
 * Pet Store Service
 * This is a pet store service.
 * OpenAPI spec version: 0.0.0
 */

import { faker } from "@faker-js/faker";
import { HttpResponse, delay, http } from "msw";

export const getPetsListResponseMock = (): Pet[] =>
  Array.from(
    { length: faker.number.int({ min: 1, max: 10 }) },
    (_, i) => i + 1
  ).map(() => ({
    age: faker.number.int({ min: 0, max: 100 }),
    kind: faker.helpers.arrayElement(["dog", "cat", "fish"] as const),
    name: faker.word.sample(),
  }));

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

クライアントに React Query を導入する

https://orval.dev/guides/react-query
筆者は React Query を使用することが多いので、設定ファイル(orval.config.cjs)を修正してクライアントに React Query を使用するようにしてみます。

orval.config.cjs
module.exports = {
  "petstore-file": {
    input: "./generated/openapi.yaml",
    output: {
      target: "./src/petstore.ts",
      client: "react-query", // 追加
      mock: true,
    },
  },
};

すると、React Query を使用したカスタムフック(usePetsList)が生成されました。これを使用して pets の一覧を取得することができそうです。

petstore.ts
/**
 * Generated by orval v6.31.0 🍺
 * Do not edit manually.
 * Pet Store Service
 * This is a pet store service.
 * OpenAPI spec version: 0.0.0
 */
import { useQuery } from "@tanstack/react-query";
import type {
  QueryFunction,
  QueryKey,
  UseQueryOptions,
  UseQueryResult,
} from "@tanstack/react-query";
export const usePetsList = <
  TData = Awaited<ReturnType<typeof petsList>>,
  TError = AxiosError<unknown>,
>(options?: {
  query?: UseQueryOptions<Awaited<ReturnType<typeof petsList>>, TError, TData>;
  axios?: AxiosRequestConfig;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } => {
  const queryOptions = getPetsListQueryOptions(options);

  const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
    queryKey: QueryKey;
  };

  query.queryKey = queryOptions.queryKey;

  return query;
};

クライアントファイル生成後にファイルをフォーマットする

orval がクライアントを生成した後にprettierを実行してフォーマットするように設定しておきます。
afterAllFilesWrite は、ファイルが書き込まれた後に実行されるフックです。

orval.config.cjs
module.exports = {
  "petstore-file": {
    input: "./generated/openapi.yaml",
    output: {
      target: "./src/petstore.ts",
      client: "react-query",
      mock: true,
    },
    hooks: {
      afterAllFilesWrite: "prettier --write", // 追加
    },
  },
};

https://orval.dev/reference/configuration/hooks#afterallfileswrite

もしくは、prettier: trueを設定することで、自動でフォーマットされるようになります。biome を使用している場合はbiome: trueを設定できます。

orval.config.cjs
module.exports = {
  "petstore-file": {
    input: "./generated/openapi.yaml",
    output: {
      target: "./src/petstore.ts",
      client: "react-query",
      mock: true,
      prettier: true, // 追加
    },
  },
};

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

Storybook でモックを利用する

msw-storybook-addonを使用して、Orval(MSW) で生成したモックを Storybook で利用します。
インストールの項目に従ってインストールします。
https://storybook.js.org/docs/get-started/install
https://storybook.js.org/addons/msw-storybook-addon

npx storybook@latest init
npm i msw msw-storybook-addon -D
npx msw init public/

preview.tsx の調整

msw-storybook-addon の設定と React Query の QueryClientProvider を追加します。

.stories/preview.tsx
import React from "react";
import type { Preview } from "@storybook/react";
import { initialize, mswLoader } from "msw-storybook-addon";
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";

// Initialize MSW
initialize();

const queryClient = new QueryClient();

const preview: Preview = {
  parameters: {
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/i,
      },
    },
  },
  loaders: [mswLoader],
  decorators: [
    (Story) => (
      <QueryClientProvider client={queryClient}>
        <Story />
      </QueryClientProvider>
    ),
  ],
};

export default preview;

ペット一覧を表示するコンポーネントを作成

雑ですが、ペット一覧を表示するコンポーネントを作成します。

pets.tsx
import { usePetsList } from "./petstore";

export const PetsPage = () => {
  const petsQuery = usePetsList();
  return (
    <ul>
      {petsQuery.data?.data.map((pet) => <li key={pet.name}>{pet.name}</li>)}
    </ul>
  );
};

モックを利用した Storybook の Story を作成

orval で生成したハンドラ(getPetsListMockHandler)をparameters.msw.handlersに設定します。

pets.stories.tsx
import { PetsPage } from "./pets";
import type { Meta, StoryObj } from "@storybook/react";
import { getPetsListMockHandler } from "./petstore";

const meta = {
  title: "Example/PetsPage",
  component: PetsPage,
  parameters: {
    msw: {
      handlers: [getPetsListMockHandler()],
    },
  },
} satisfies Meta<typeof PetsPage>;

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

export const Primary: Story = {};

ストーリーにアクセスすると、モックデータが表示されることを確認できました。

まとめ

TypeSpec を使用することで yaml で記述するよりもだいぶスマートに OpenAPI ファイルを生成できました。今回の例では記述量が少ないのであまり実感はないですが、大規模な API を提供する場合には非常に便利になると思います。
また、Orval を使用してクライアントコード・モックを生成できているので、インポートして利用するだけですぐ動作確認ができるのは非常に便利だなと思いました。
以上、TypeSpec、Orval、Storybook を使用してフロントエンドのモック生成を自動化する方法を紹介しました。何かの参考になれば嬉しいです。

ファンタラクティブテックブログ

Discussion