🐟

OrvalでOpenAPIからHonoとfetch APIクライアントを自動生成しREST APIでも堅牢なスキーマ駆動開発を実現する

2024/08/25に公開

はじめに

OrvalというOpenAPIからREST APIのクライアントやzodのバリデーション定義、mswでのモック定義を自動生成するツールですがfetch APIHonoもサポートされています。

https://orval.dev/

それらの自動生成を用いる事でREST APIを用いてTypescriptでのバックエンド、フロントエンドを開発する際にも堅牢なスキーマ駆動開発を行う事ができると考えています。
この記事では以下のアプリケーションを構築しながらその方法について解説します。

  • HonoでのAPIアプリケーション開発
  • fetch APINext.jsで使用したクライアントアプリケーション開発

解説

Orvalリポジトリにあるサンプルアプリケーションをベースに解説します。

https://github.com/orval-labs/orval/tree/master/samples/hono/hono-with-fetch-client

アプリケーション構成

まずはアプリケーションの全体像を確認します。
アプリケーション全体は大きく以下の構成になっています。

  • hono-app: Honoで作成したAPIアプリケーション
  • next-app: Next.jsで作成したクライアントアプリケーション
  • orval.config.ts: Orvalの設定ファイル
  • petstore.yaml: OpenAPIのスキーマファイル

Orvalによりpetstore.yamlの定義に基づいたHonoの雛形をhono-appに、fetch APIのクライアントをnext-appにそれぞれ自動生成します。

$ pwd
orval/samples/hono/hono-with-fetch-client
$ tree -L 2
.
├── README.md
├── hono-app
│   ├── README.md
│   ├── node_modules
│   ├── package.json
│   ├── src
│   ├── tsconfig.json
│   └── wrangler.toml
├── next-app
│   ├── README.md
│   ├── app
│   ├── next-env.d.ts
│   ├── next.config.mjs
│   ├── node_modules
│   ├── package.json
│   ├── postcss.config.mjs
│   ├── public
│   ├── tailwind.config.ts
│   └── tsconfig.json
├── node_modules
├── orval.config.ts
├── package.json
└── petstore.yaml

Orvalでの自動生成

サンプルアプリのルートディレクトリに存在するorval.config.tsの設定は以下の通りで二つの自動生成の設定をしています。

  • petstoreClient: fetch APIを用いたAPIクライアントの設定
  • petstoreApi: Honoの雛形を生成するための設定
orval.config.ts
import { defineConfig } from 'orval';

export default defineConfig({
  petstoreClient: {
    input: {
      target: './petstore.yaml',
    },
    output: {
      mode: 'tags-split',
      client: 'fetch',
      target: 'next-app/app/gen/',
      schemas: 'next-app/app/gen/models',
      clean: true,
      baseUrl: 'http://localhost:8787',
      mock: true,
    },
  },
  petstoreApi: {
    input: {
      target: './petstore.yaml',
    },
    output: {
      mode: 'split',
      client: 'hono',
      target: 'hono-app/src/petstore.ts',
      override: {
        hono: {
          handlers: 'hono-app/src/handlers',
        },
      },
    },
  },
});

各設定項目についての解説は割愛するので詳細はOrvalのドキュメントを参照してください。

orvalの実行

orvalを実行します。

yarn orval

🍻 Start orval v7.0.1 - A swagger client generator for typescript
🎉 petstoreClient - Your OpenAPI spec has been converted into ready to use orval!
🎉 petstoreApi - Your OpenAPI spec has been converted into ready to use orval!

petstore.yamlの定義に基づきそれぞれのコードが自動生成されます。

  • Honoの雛形がhono-appに生成
  • fetch APIのクライアントがnext-appに生成

Honoアプリケーション

自動生成されたHonoの雛形を確認します。
hono-appyarn create hono-appで作成したプロジェクトをベースにしています。
src配下に配置されているファイルがOrvalによって生成されたファイルです。

ファイル構成

cd hono-app
tree src/
src/
├── handlers
│   ├── createPets.ts
│   ├── listPets.ts
│   ├── showPetById.ts
│   └── updatePets.ts
├── index.ts
├── petstore.context.ts
├── petstore.schemas.ts
├── petstore.ts
├── petstore.validator.ts
└── petstore.zod.ts
  • petstore.ts: Honoの初期化とエンドポイントの定義
  • handlers: エンドポイントごとの処理を記述
  • petstore.schemas.ts: リクエスト/レスポンスのスキーマ定義
  • petstore.validator.ts: バリデーションの定義
  • petstore.zod.ts: バリデーションのスキーマ定義
  • petstore.context.ts: エンドポイントのコンテキスト定義

index.tsHonoプロジェクトを作成した際に作成されたファイルです。以後使用しないので削除しても問題ありません。

雛形の実装

handlersに雛形が生成されているので各エンドポイントの処理を記述します。
ここではサンプルとしてhandlers/listPets.tsJSONで固定値を返却するだけの実装を行います。

handlers/listPets.ts
import { createFactory } from 'hono/factory';
import { zValidator } from '../petstore.validator';
import { ListPetsContext } from '../petstore.context';
import { listPetsQueryParams, listPetsResponse } from '../petstore.zod';

const factory = createFactory();

export const listPetsHandlers = factory.createHandlers(
  zValidator('query', listPetsQueryParams),
  zValidator('response', listPetsResponse),
  async (c: ListPetsContext) => {
+    return c.json([
+      {
+        id: 1,
+        name: 'doggie',
+        tag: 'dog',
+      },
+    ]);
  },
);

アプリケーションの起動

Honoアプリケーションを起動します。

yarn dev

http://localhost:8787にアクセスする事でAPIのエンドポイントにアクセスできます。

curl http://localhost:8787/pets

[{"id":1,"name":"doggie","tag":"dog"}]

Next.jsアプリケーション

自動生成されたfetch APIのクライントを確認します。
next-appyarn create next-appで作成したプロジェクトをベースにしています。
app/gen/に配置されているファイルがOrvalによって生成されたファイルです。

tree app/gen/
app/gen/
├── models
│   ├── createPetsBodyItem.ts
│   ├── error.ts
│   ├── index.ts
│   ├── listPetsParams.ts
│   ├── pet.ts
│   └── pets.ts
└── pets
    ├── pets.msw.ts
    └── pets.ts
  • models: リクエスト/レスポンスのスキーマ定義
  • pets/pets.ts: エンドポイントごとのクライアント処理を記述
  • pets/pets.msw.ts: モックサーバーの設定

エンドポイントごとにディレクトリが作成され、クライアント処理とmswによるモック実装のファイルが作成されます。
zodによるバリデーション定義も生成することも可能ですがサンプルでは割愛しているので興味があればOrvalのドキュメントを参照してください。

APIクライアントの使用

app/pets.tsxを作成して以下の様にAPIクライアントを使用します。

app/pets.tsx
import { listPets } from './gen/pets/pets';

export default async function Pets() {
  const { data: pets, status } = await listPets();

  return (
    <div>
      <h1 className="text-4xl">Pets by server actions</h1>

      <ul>
        {pets.map((pet) => (
          <li key={pet.id}>tag: {pet.tag}</li>
        ))}
      </ul>

      <h2 className="text-xl">Status: {status}</h2>
    </div>
  );
}

listPets関数はOrvalから自動生成された関数です。

app/gen/pets/pets.tsx
export const listPets = async (
  params?: ListPetsParams,
  options?: RequestInit,
): Promise<listPetsResponse> => {
  const res = await fetch(getListPetsUrl(params), {
    ...options,
    method: 'GET',
  });
  const data = await res.json();

  return { status: res.status, data };
};

Petコンポーネントはapp/page.tsxで呼び出されています。

app/page.tsx
import Pets from './pets';

export default function Home() {
  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
      <Pets />
    </main>
  );
}

アプリケーションの起動

Next.jsアプリケーションを起動します。

yarn dev

http://localhost:3000にアクセスする事でクライアントアプリケーションにアクセスします。
Honoで構築したAPIにアクセスして取得したデータが表示されている事が確認できます。

Orvalのwatchオプション

最初に自動生成する際に実行したOrvalコマンドですがwatchオプションがあります。
orvalコマンドに--watchを付与して実行する事でwatchモードで起動し、OpenAPIのスキーマファイルが変更された際に自動でクライアントコードを再生成することができます。

yarn orval --watch

🍻 Start orval v7.0.1 - A swagger client generator for typescript
Watching for changes in "/app/samples/hono/hono-with-fetch-client/petstore.yaml" | "/app/samples/hono/hono-with-fetch-client/petstore.yaml"
Change detected: add /app/samples/hono/hono-with-fetch-client/petstore.yaml
petstoreClient: Cleaning output folder
🎉 petstoreClient - Your OpenAPI spec has been converted into ready to use orval!
🎉 petstoreApi - Your OpenAPI spec has been converted into ready to use orval!

これにより以下の開発フローを実現することができREST APIでのシームレスなスキーマファースト開発を行うことができます。

  1. OpenAPIのスキーマファイルを変更
  2. Honofetch APIのクライアントコードが自動で再生成

実際に開発しているショートムービーをサンプルアプリのREADMEに添付しているのでご覧ください。

おわりに

fetch APIのクライアントコードをNext.jsで使い、Honoで構築したREST APIを呼び出すアプリケーションを開発することができました。
これによりREST APIを用いてTypescriptでのバックエンド、フロントエンドを開発する際にも堅牢なスキーマ駆動開発をシームレスに行う事ができるのではないかと考えています。
Orvalは他にもOpenAPIからクライアントコードを自動生成することができ様々なケースで活用できると思いますので是非試してみてください。

https://orval.dev/guides/set-base-url

参考

Discussion