Orvalは、OpenAPI(Swagger)のスペックを読み込んで、フロントエンドのAPIレイヤーを代わりに書いてくれるコードジェネレーターです。エンドポイントごとの型付き関数かReact Queryのフック、コントラクトに正確に一致するTypeScriptのモデル、そして指定すれば、Fakerのデータが入ったMSWのモックハンドラーまで生成します。設定ファイルを1つコミットしてorvalを実行し、スペックが変わったらまた実行する。それだけです。
このシリーズの最初の記事で、「コードを書く前にOpenAPIのコントラクトに合意しよう」という話をしました。今回はそのコントラクトを実際に「元が取れるもの」にしてくれるツールの話です。
フロントエンドのAPIレイヤーを手書きする作業は、楽しいと公言する人がいない類の仕事です。バックエンドのドキュメントから型を書き写し、40回目の同じリクエストラッパーを書き、3週間後に誰かがフィールド名を変えて、呼び出しの半分が静かに壊れる。Orvalはこの仕事そのものを消してくれます。OpenAPIスペックを指定すれば、クライアントコードを生成してくれるのです。全エンドポイント分の型付き関数、コントラクトに正確に一致するモデル、そしてバックエンドが存在する前から開発に使えるモック。
フロントエンドチームが気に入る理由#
過小評価されがちなのがモック生成です。スペックに合意した時点で、フロントエンドは偽のサーバーを立てて、UI全体をそれに向かって作り込めます。永遠に「もうすぐできます」のエンドポイントを待つ必要はありません。本物のバックエンドができたら、クライアントの向き先を変えるだけで、だいたいそのまま動きます。両側が同じドキュメントから作られているからです。
もうひとつの利点は、退屈なコードが「構造的に正しい」状態になることです。手書きのリクエストボディでフィールド名を打ち間違える人はいなくなります。そもそも手書きのリクエストボディを書く人がいなくなるので。
バックエンドチームにも嬉しい理由#
スペックが唯一の情報源になると、日々の会話が変わります。「このエンドポイントって何を返すんでしたっけ?」というチャットのスレッドの代わりに、フロントエンドは生成された型を読めばいい。バックエンド開発者は割り込まれる回数が減り、フィードバックは早く届くようになります。フロントエンドがモック相手に作ってみて「このAPI、使いにくいな」と感じたら、それが分かるのは本番投入後ではなく、まだ変更が安上がりなうちです。
ワークフローへの組み込み方#
けっして凝ったことはしていません。バックエンドがスペックを提案し、両チームでレビューし、フロントエンドはOrvalでクライアントとモックを生成して作り始め、バックエンドは同じコントラクトに沿って本物を実装する。スペックが変わったら両側で再生成すれば、何が壊れたかはコンパイラが正確に教えてくれます。この最後の部分こそが本質です。フロントエンドとバックエンドのズレが、本番のバグではなくコンパイルエラーになるのです。
セットアップ#
まずは開発用の依存関係としてインストールします。生成されたコードがimportするパッケージも一緒に入れておきましょう。React Queryのフックとモックを使うなら、TanStack Query、MSW、Fakerです。
npm install --save-dev orval msw @faker-js/faker
npm install @tanstack/react-query次に、プロジェクトのルートにorval.config.tsを置きます。Orval公式ドキュメントの例とほぼ同じものです。
import { defineConfig } from 'orval';
export default defineConfig({
petstore: {
input: {
target: './openapi.yaml', // a path or a URL
},
output: {
mode: 'single',
target: './src/api/petstore.ts',
schemas: './src/api/model',
client: 'react-query',
httpClient: 'fetch',
mock: true, // MSW handlers plus Faker data
override: {
fetch: { includeHttpResponseReturnType: false },
},
},
},
});何が生成されるかはclientで決まります。素のfetchやaxiosの関数、React Query・Vue Query・Svelte Query・SWR用のフック、Angularのサービス、Zodのスキーマなどです。includeHttpResponseReturnType: falseの行は、fetchクライアントが{ data, status }ではなくレスポンスボディそのものを返すようにする設定で、コンポーネント側が欲しいのはたいていこちらです。
コマンドを誰も覚えなくて済むように、scriptsに登録しておきます。
{
"scripts": {
"api": "orval"
}
}npm run apiでフックとモデルが書き出されます。あとはコンポーネントから普通のReact Queryのフックと同じように使うだけで、型は最初から埋まっています。
import { useListPets } from './api/petstore';
export function PetList() {
const { data: pets, isLoading } = useListPets();
if (isLoading) return <p>Loading…</p>;
return (
<ul>
{pets?.map((pet) => <li key={pet.id}>{pet.name}</li>)}
</ul>
);
}スペックでフィールド名を変えてnpm run apiをもう一度実行すれば、pet.nameを使っている箇所が全部赤くなります。上で書いたズレのチェックがこれで、実行コストはゼロです。
モックを相手に開発する#
mock: trueにすると、Orvalはエンドポイントごとに、レスポンスのスキーマどおりの形をしたFakerのデータを返すMSWのリクエストハンドラーと、それらをまとめて返す関数も書き出します。そのリストを、ブラウザならMSWのsetupWorkerに、テストならsetupServerに渡せば、アプリは最初からコントラクトに一致した偽のバックエンドを相手に動きます。各ハンドラーは上書き用の値も受け取れるので、テストで特定のペイロードや空のリスト、エラー状態を返したいときも、誰かがフィクスチャを手書きする必要はありません。
フロントエンドが初日から作業を始められるのは、この部分のおかげです。モックは誰かが別途メンテナンスしなければならない成果物ではありません。クライアントと同じスペックから毎回再生成されるので、スペックに置いていかれようがないのです。
Orvalとopenapi-generator、openapi-typescript、Hey APIの比較#
どれもOpenAPIスペックから出発しますが、どこまで生成するかが違います。
| 生成するもの | 向いているケース | |
|---|---|---|
| Orval | 型付きのクライアント関数やフレームワーク用のフック(React Query、SWR、Vue、Svelte、Angular)、モデル、MSWのモック、Zodのスキーマ | 1つの設定ファイルからフックとモックをまとめて得たいTypeScriptのフロントエンド |
| openapi-generator | 数十の言語のクライアントとサーバースタブ(TypeScriptも含む) | 同じスペックからJava、Kotlin、Swift、TypeScriptを生成しなければならないチーム |
| openapi-typescript | 型のみ。小さな型付きfetchラッパーのopenapi-fetchと組み合わせて使う | 型だけが欲しくて、ランタイムのコードはほとんど要らない場合 |
Hey API(@hey-api/openapi-ts) | 型付きのSDK。TanStack QueryやZodなどのプラグインあり | Orvalと守備範囲が近く、プラグインの仕組みを中心に作られている |
型だけで足りるなら、openapi-typescriptが一番軽くて、文句のつけようがありません。openapi-generatorが真価を発揮するのは、1つのスペックから複数の言語向けにコードを生成しなければならないときです。TypeScriptの出力もちゃんと動きますが、読んでいると、まずJava向けに書かれたものだという感触があります。OrvalとHey APIのどちらにするかは、ほぼ好みと、どちらの出力コードを読みたいか次第です。私がOrvalを使い続けているのは、フックと同じ1回の実行でモックも出てくるからで、2つのチームの働き方を変えたのはまさにこの機能でした。
派手なツールではありません。ただ、チーム間の言い争いをひとつのカテゴリごと静かに消してくれる。それと引き換えなら、私はいつでも喜んで使います。
