TypeSpec、Orval、Storybook を使ってフロントエンドのモック生成を自動化する
はじめに
フロントエンド開発において、効率的かつ一貫性のあるモック生成は非常に重要です。本記事では TypeSpec、Orval、Storybook の 3 つのツールを使用して自動生成でモックを実現する方法を紹介します。
TypeSpec は、大規模な API を提供するために Microsoft が開発し、使用している新しい API 記述言語です。
Orval は、OpenAPI 仕様から TypeScript のクライアントコードを生成するツールです。これにより、最新の API 仕様に基づいたクライアントコードを常に保持し、API との通信がスムーズに行えるようになります。
Storybook は、コンポーネントを独立して開発・テストするためのインタラクティブなツールです。コンポーネントの見た目や動作を個別に確認できるため、UI の一貫性を保ちながら効率的に開発を進めることができます。
TypeSpec を使用して OpenAPI ファイルを生成する
セットアップ
以下を参考に、TypeSpec のインストール、拡張機能のダウンロードなどをします。
出力先が気になったので generated ディレクトリの直下に出力されるように変更してみました。
output-dir: "{cwd}/generated"
options:
"@typespec/openapi3":
emitter-output-dir: "{output-dir}/"
emit:
- "@typespec/openapi3"
TypeSpec への記述と OpenAPI ファイルの生成
TypeSpec のREADME記載の tsp ファイル(PetStore)を拝借して OpenAPI ファイルの生成を試してみます。
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 ファイルが生成されました。
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 を作成し、簡単に設定を記述します。
module.exports = {
"petstore-file": {
input: "./generated/openapi.yaml",
output: "./src/petstore.ts",
},
};
orval --config ./orval.config.cjs
を実行してみると型定義(PetKind, Pet)とクライアントコード(petsList)が生成されました。
/**
* 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)を修正して、モックを生成するようにします。
module.exports = {
"petstore-file": {
input: "./generated/openapi.yaml",
output: "./src/petstore.ts",
},
};
すると faker-js でモックデータを生成する関数(getPetsListResponseMock)と、msw でモックをハンドルする関数(getPetsListMockHandler)が生成されました 🎉
/**
* 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 を導入する
筆者は React Query を使用することが多いので、設定ファイル(orval.config.cjs)を修正してクライアントに React Query を使用するようにしてみます。
module.exports = {
"petstore-file": {
input: "./generated/openapi.yaml",
output: {
target: "./src/petstore.ts",
client: "react-query", // 追加
mock: true,
},
},
};
すると、React Query を使用したカスタムフック(usePetsList)が生成されました。これを使用して pets の一覧を取得することができそうです。
/**
* 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 は、ファイルが書き込まれた後に実行されるフックです。
module.exports = {
"petstore-file": {
input: "./generated/openapi.yaml",
output: {
target: "./src/petstore.ts",
client: "react-query",
mock: true,
},
hooks: {
afterAllFilesWrite: "prettier --write", // 追加
},
},
};
もしくは、prettier: true
を設定することで、自動でフォーマットされるようになります。biome を使用している場合はbiome: true
を設定できます。
module.exports = {
"petstore-file": {
input: "./generated/openapi.yaml",
output: {
target: "./src/petstore.ts",
client: "react-query",
mock: true,
prettier: true, // 追加
},
},
};
Storybook でモックを利用する
msw-storybook-addon
を使用して、Orval(MSW) で生成したモックを Storybook で利用します。
インストールの項目に従ってインストールします。
npx storybook@latest init
npm i msw msw-storybook-addon -D
npx msw init public/
preview.tsx の調整
msw-storybook-addon の設定と React Query の QueryClientProvider を追加します。
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;
ペット一覧を表示するコンポーネントを作成
雑ですが、ペット一覧を表示するコンポーネントを作成します。
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
に設定します。
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 を使用してフロントエンドのモック生成を自動化する方法を紹介しました。何かの参考になれば嬉しいです。
ユーザーファーストなサービスを伴に考えながらつくる、デザインとエンジニアリングの会社です。エンジニア積極採用中です!hrmos.co/pages/funteractive/jobs
Discussion
有用なポスティングありがとうございます
現在の例のGitHubコードを見ることはできますか?
コメントありがとうございます。こちらに公開しました!
以下で動作を確認してみてください。