はじめに
本日はOpenAPIで定義したAPI定義について、半自動でAPIモックを作る方法をご紹介します。
初心者向けですが、対象読者は基本的なReactやRestful APIの知識がある前提とします。
APIモックを作る必要性は?
一つの機能をフロントエンドとバックエンドで別々に作成していく場合に、APIモックがあるとバックエンド側の実装を待つことなくフロントエンドの開発を進められることができます。
個人的にはバックエンドとフロントエンドで分けて開発するのはデメリットが多いなと思うのですが、教育にもそこまでコストかけられないなどあれば別々に開発できるような環境を整備する必要があると思います。
なぜこの組み合わせなのか
相性が良いです。
OrvalというReact Client 自動生成ツールを使用することでOpenAPIスキーマからクライアントコードを生成することが可能です。
Orvalは以下の機能を実現するために作られています。
- Typescriptモデルの作成
- httpクライアント作成
- mswを用いたモック関数の作成
デフォルトの機能としてモック関数の生成が備わっているため自作でモック関数を作成するより遥かに作成工数を削減することが可能です。
環境
今回は検証環境のため、buildが爆速なviteを使用しようと思います。
yarn、npmでも問題ないです。
- pnpm
- vite v4
- react v18
"@faker-js/faker": "^8.0.2",
"@tanstack/react-query": "^4.35.0",
"axios": "^1.5.0",
"msw": "^1.3.0",
"orval": "^6.17.0",
"prettier": "^3.0.3",
"react": "^18.2.0",
"react-dom": "^18.2.0"
資材インストール
pnpm addで対象のライブラリをインストールします。
orval、@faker-js/faker、mswは開発時のみ使用するライブラリなので -d オプションをつけてインストールします。
pnpm add -d orval @faker-js/faker msw
pnpm add @tanstack/react-query axios
サンプルAPI定義作成
/todosというパスでGETリクエストを送信すると以下のようなjson配列を返すAPIを定義します。
[{
id: 1,
title: "サンプルタイトル",
description: "サンプル説明",
isComplete: false
}]
openapi.yamlというファイルを以下のように作成します。
paths:
/todos:
get:
summary: Returns a list of todos.
description: Optional extended description in CommonMark or HTML.
responses:
'200': # status code
description: A JSON array of user names
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/ToDo'
components:
schemas:
ToDo:
type: object
title: ToDo
properties:
id:
type: integer
title:
type: string
maxLength: 64
minLength: 1
description:
type: string
maxLength: 256
minLength: 1
isComplete:
type: boolean
required:
- id
- title
- isComplete
内容はシンプルで、/todosというgetリクエストが来た際にToDoモデルの配列を返すという内容になります。
コードの自動生成
Orvalからコードを自動生成
orvalとは先ほど定義したopenapi.yamlからコードを自動生成するライブラリになります。
openapi-generator-cliというライブラリでもopenapi.yamlファイルからコードの自動生成は可能ですが、orvalではどちらかというと必要最低限のclientコードのみ作成することが可能のため今回のような需要に適しております。
orvalの設定ファイルをルート直下に以下のように作成します。orval.config.ts
ポイントは以下です。
- openapi.yamlファイルをinputに指定
- targetで生成するフォルダを指定
- clientはreact-queryで生成する
- tags毎にファイルを自動生成する
- prettierでコードフォーマットする
- mockを作成する
import { defineConfig } from "orval";
export default defineConfig({
sample: {
input: {
target: "./openapi.yaml"
},
output: {
target: "./src/api/",
client: "react-query",
mode: "tags",
clean: true,
prettier: true,
mock: true,
},
}
});
scriptsを実行しやすいようにpackage.jsonのscriptsにorvalコマンドを定義しておきましょう。
"orval": "orval --config ./orval.config.ts"
$ pnpm orval
コマンドを叩きコードを自動生成します。
murakami@murakami-pc:~/openapi-and-faker$ pnpm orval
> openapi-and-faker@0.0.0 orval /home/murakami/openapi-and-faker
> orval --config ./orval.config.ts
🍻 Start orval v6.17.0 - A swagger client generator for typescript
sample: Cleaning output folder
outputで指定したフォルダにコードが自動生成されていることが確認できます。
OpenAPI定義でタグを指定していないためファイル名がdefault.tsとして作成されたのですが、タグでtodoなどをつけるとtodo.tsとしてファイルが作成されます。
openapi.yamlファイルは肥大化するのでタグをつけることをおすすめします。
自動生成されたコード
自動生成されたdefault.tsを開くと以下のようになっています。
export const getTodos = (
options?: AxiosRequestConfig,
): Promise<AxiosResponse<ToDo[]>> => {
return axios.default.get(`/todos`, options);
};
export const getGetTodosQueryKey = () => [`/todos`] as const;
export const getGetTodosQueryOptions = <
TData = Awaited<ReturnType<typeof getTodos>>,
TError = AxiosError<unknown>,
>(options?: {
query?: UseQueryOptions<Awaited<ReturnType<typeof getTodos>>, TError, TData>;
axios?: AxiosRequestConfig;
}): UseQueryOptions<Awaited<ReturnType<typeof getTodos>>, TError, TData> & {
queryKey: QueryKey;
} => {
const { query: queryOptions, axios: axiosOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetTodosQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof getTodos>>> = ({
signal,
}) => getTodos({ signal, ...axiosOptions });
return { queryKey, queryFn, ...queryOptions };
};
/**
* @summary Returns a list of todos.
*/
export const useGetTodos = <
TData = Awaited<ReturnType<typeof getTodos>>,
TError = AxiosError<unknown>,
>(options?: {
query?: UseQueryOptions<Awaited<ReturnType<typeof getTodos>>, TError, TData>;
axios?: AxiosRequestConfig;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } => {
const queryOptions = getGetTodosQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
};
export const getGetTodosMock = () =>
Array.from(
{ length: faker.datatype.number({ min: 1, max: 10 }) },
(_, i) => i + 1,
).map(() => ({
id: faker.datatype.number({ min: undefined, max: undefined }),
title: faker.random.word(),
description: faker.helpers.arrayElement([faker.random.word(), undefined]),
isComplete: faker.datatype.boolean(),
}));
export const getDefaultMSW = () => [
rest.get('*/todos', (_req, res, ctx) => {
return res(
ctx.delay(1000),
ctx.status(200, 'Mocked status'),
ctx.json(getGetTodosMock()),
);
}),
];
簡単にですが一つ一つ見ていきましょう。
useGetTodos
axiosを用いてAPIアクセスするgetTodoをreac-queryの形式で使用することができます。
React Query形式でパス名・APIメソッドについて気にすることなく呼び出すことが可能になります。
第一引数が query?: UseQueryOptions<Awaited<ReturnType<typeof getTodos>>, TError, TData>
となっているため、react-queryのオプションを第一引数で指定することが可能です。
const { data } = useGetTodos({
query: {
onSuccess: () => {
console.log('success');
},
onError: () => {
console.log('success');
},
},
});
getGetTodosMock
Todoの配列モックオブジェクトを作成します。
fakerライブラリからランダム値のデータが作成されます。
id: faker.datatype.number({ min: undefined, max: undefined }),
title: faker.random.word(),
description: faker.helpers.arrayElement([faker.random.word(), undefined]),
isComplete: faker.datatype.boolean(),
getDefaultMSW
rest.get(‘*/todos’)のリクエストが来た際にgetGetTodosMockメソッドを返却します。
一点注意が必要で、mswはworkerプロセスが起動しているときのみ/todosのAPIをモックします。
そのためgetTodosMockで定義したデータを返却させるにはmswのworkerプロセスを起動しなければいけません。
アプリケーション内でworker.startを呼び出します。
setupWorkerでgetDefaultMSWを呼び出し、worker.start()からworkerを起動します。
import { getDefaultMSW, useGetTodos } from './api/default';
import { setupWorker } from 'msw';
const worker = setupWorker(...getDefaultMSW());
worker.start();
mswのworkerプロセスセットアップ
mswのworkerプロセスを起動する為に、ワーカーファイルを作成する必要があります。
下記コマンドを実行します。
$ pnpm msw init public/ --save
publicフォルダにmockServiceWorker.jsが作成されていることを確認します。
Reactで自動生成したコードを呼び出す
App.tsxを修正
以下のようにApp.tsxを修正し、先ほど自動生成したuseGetTodosメソッドを呼び出しましょう。
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { getDefaultMSW, useGetTodos } from './api/default';
import { setupWorker } from 'msw';
import React from 'react';
const worker = setupWorker(...getDefaultMSW());
worker.start();
function App() {
const queryClient = new QueryClient();
return (
<QueryClientProvider client={queryClient}>
<Todo />
</QueryClientProvider>
);
}
const Todo: React.FC = () => {
const { data: response } = useGetTodos({
query: {
onSuccess: (res) => {
console.log(res);
},
onError: () => {
console.log('success');
},
},
});
return (
<>
{response?.data.map((item) => {
return (
<div>
ID: {item.id}, Title: {item.title} , Description: {item.description}
</div>
);
})}
</>
);
};
export default App;
$ pnpm dev
コマンドからアプリを起動します。
アプリが立ち上がると以下のようにuseGetTodosで呼び出したdataが画面に映っているはずです!
これでfakerより自動で作成されたモックデータが返却されるようになりました。
今回は初回のため色々とコード紹介しましたが、次回以降はAPI定義を作成しuse○○という自動生成されたコードを呼び出すだけでAPIモックを使用することが可能です。
自動で生成されたモックデータも約に立つとは思いますが、画面によっては指定したIDを出したい、指定した長いタイトルを出したいなどの要望が出てくると思います。
そこで最後に自動で作成したデータではなく、指定したデータを返却するようにカスタマイズするtipsを紹介します。
Custom API Mockの作成
上記で作成したgetGetTodosMockとgetDefaultMSWとは別に、固定文言を返すgetGetCustomTodosMockとgetCustomMSWメソッドを作成します。
import { rest } from 'msw';
export const getGetCustomTodosMock = () =>
Array.from({ length: 10 }, (_, i) => i + 1).map((_, index) => ({
id: index,
title: 'タイトル' + index,
description: '説明' + index,
isComplete: index % 2 === 0 ? false : true,
}));
export const getCustomMSW = () => [
rest.get('*/todos', (_req, res, ctx) => {
return res(
ctx.delay(1000),
ctx.status(200, 'Mocked status'),
ctx.json(getGetCustomTodosMock()),
);
}),
];
App.tsxのsetupWorkerでgetDefaultMSW()より前にgetCustomMSWを渡します。
const worker = setupWorker(...getCustomMSW(), ...getDefaultMSW());
worker.start();
これで好きなデータを返すモックAPIを作成できましたね!
まとめ
orvalとmswを使用することでモックデータを簡単に作成することができました。
モックデータを上手く作ることで効率よく開発を進めれたらと思います。
一応サンプルコードは以下です。
コメント