0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【API定義書は不要?】Hono × Zod × Orvalで自動生成 & 型を貫通

0
Last updated at Posted at 2026-02-05

はじめに

開発現場でこんな経験はありませんか?

「バックエンドの実装が変わったのに、API定義書(Swagger/OpenAPI)の更新が漏れていた」「フロントエンドの実装中に、APIのレスポンス型のミスで手戻りが発生した」。

これらはすべて、「コード」と「ドキュメント」が分離していることが原因です。

本記事では、Hono (@hono/zod-openapi)Orval を組み合わせることで、「バックエンドのコードを書けば、フロントエンドのデータ取得フック(React Query)まで全自動で生成される」 究極の型安全フローを構築します。

API定義書を書く時間は、もう必要ありません。

この記事で学べること

  • Hono + Zod:コードファーストでOpenAPI仕様書を自動生成する方法
  • Orval:OpenAPIからTypeScript型定義とReact Queryフックを自動生成する方法
  • DX(開発者体験):バックエンドの変更を即座にフロントエンドに検知させる堅牢なワークフロー

今回の構成

今回はシンプルさを重視し、全てローカル環境(Node.js)で完結させます。

  • Frontend: Next.js (React) + TanStack Query
  • Backend: Hono (Node.js Adapter) + Zod
  • Generator: Orval (OpenAPI to React Query hooks)

2. 環境・前提条件

ハンズオンを進めるにあたり、以下の準備が必要です。

  • Node.jsのインストール: 公式リンク
    • インストールすると npm も自動的に利用可能になります。

検証環境

今回の検証に使用した環境は以下の通りです。2026年時点のスタンダードな構成を想定しています。

項目 バージョン 備考
OS macOS Sequoia v15.3.2 -
Runtime (Node.js) v22.22.0 LTS推奨
Backend
hono v4.11.7 Webフレームワーク
@hono/zod-openapi v1.2.1 OpenAPI拡張
@hono/swagger-ui v0.2.1 ドキュメントUI
@hono/node-server v1.19.9 Node.jsアダプタ
zod v4.3.6 スキーマ定義
Frontend
next v16.1.6 App Router
@tanstack/react-query v5.90.20 データフェッチ
axios v1.13.4 HTTPクライアント
orval v7.13.2 コード生成

ライブラリのバージョンについて

HonoやNext.jsは進化が速いライブラリです。本記事のコードは執筆時点のバージョンで動作確認を行っていますが、環境構築時はバージョンを確認しつつ、公式ドキュメントも併せてご確認ください。

3. 技術解説(仕組み・料金など)

Hono とは

Web標準(Web Standards)に準拠した、超高速・軽量なWebフレームワークです。

AWS Lambda、Cloudflare Workers、Deno、Node.jsなど、あらゆる環境で動作するのが特徴です。今回は zod-openapi という拡張機能を使い、「実装コードからドキュメントを生成する」 アプローチを取ります。

Orval とは

OpenAPI(Swagger)のJSON/YAML定義を読み込み、TypeScriptの型定義だけでなく、データフェッチ用のReact Queryフックまで自動生成してくれる 強力なツールです。

「型定義ファイル」を作るツールは他にもありますが、「APIを叩く関数」まで作ってくれる点でOrvalは生産性が段違いです。

料金・コストの目安

💡 今回の構成はすべてオープンソースであり、ローカル開発であれば無料です。

クラウドへデプロイする場合も、Honoは軽量なため、AWS Lambda等の従量課金サービスと相性が良く、待機コストを極限まで抑えられます。

4. 実装ステップ

最終的なディレクトリ構成イメージ

作業を始める前に、ゴールとなるディレクトリ構成を確認しておきましょう。
バックエンド(API)フロントエンド(Next.js) を分離した構成で進めます。

my-app/                  # プロジェクトルート  
├── backend/             # Hono (API Server)  
│   ├── src/index.ts  
│   └── package.json  
└── frontend/            # Next.js (Client App)  
    ├── src/gen/         # -> Orvalで自動生成されるコード  
    ├── src/app/         
    ├── orval.config.ts  # -> Orvalの設定  
    └── package.json

手順1. バックエンド構築 (Hono + Zod)

まずはHonoでサーバーを立ち上げ、API定義を記述します。

  1. プロジェクトディレクトリの作成と初期化

    mkdir backend  
    cd backend  
    npm init \-y
    
  2. 必要なパッケージのインストール

    # HonoとOpenAPI関連のパッケージをインストール
    # ドキュメント表示用に @hono/swagger-ui も追加します
    npm install hono @hono/zod-openapi @hono/swagger-ui zod @hono/node-server
    
    # 開発用(TypeScript実行環境)
    npm install -D tsx typescript @types/node
    
    mkdir src
    touch src/index.ts
    
  3. ソースコードの実装
    backend/src/index.ts に、以下のコードを記述します。 これが「仕様書」兼「実装」になります。

    import { serve } from '@hono/node-server'
    import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi'
    import { swaggerUI } from '@hono/swagger-ui' // 追加
    import { cors } from 'hono/cors'
    
    // 通常のHonoではなく、OpenAPI機能付きのHonoインスタンスを作成
    const app = new OpenAPIHono()
    
    // フロントエンド(http://localhost:3000等)からのアクセスを許可
    // ※実務では origin: 'http://localhost:3000' のように厳密に指定します
    app.use('/*', cors())
    
    // --- 1. スキーマ定義 (Zod) ---
    // ここで定義した型が、そのままOpenAPIのドキュメントとTSの型になります
    const UserSchema = z.object({
      id: z.string().openapi({ example: '123' }),
      name: z.string().openapi({ example: 'John Doe' }),
      age: z.number().openapi({ example: '42' }),
    })
    
    // --- 2. ルート定義 ---
    // リクエストとレスポンスの形式を厳密に定義します
    const route = createRoute({
      method: 'get',
      path: '/users/{id}',
      request: {
        params: z.object({
          id: z.string().min(1).openapi({ param: { name: 'id', in: 'path' }, example: '123' }),
        }),
      },
      responses: {
        200: {
          content: {
            'application/json': {
              schema: UserSchema,
            },
          },
          description: 'ユーザー情報を取得',
        },
      },
    })
    
    // --- 3. 実装 ---
    // 定義したルートに基づいて処理を書きます
    app.openapi(route, (c) => {
      const { id } = c.req.valid('param')
    
      // 本来はDBから取得しますが、今回はダミーデータを返します
      return c.json({
        id,
        name: 'Generated User',
        age: 25,
      })
    })
    
    // --- 4. OpenAPIドキュメントの配信 ---
    // /doc にアクセスするとOpenAPIのJSONが返るように設定
    app.doc('/doc', {
      openapi: '3.0.0',
      info: {
        version: '1.0.0',
        title: 'My API',
      },
    })
    
    // Swagger UIの表示設定(追加)
    // /ui にアクセスすると、見やすいドキュメントが表示されます
    app.get('/ui', swaggerUI({ url: '/doc' }))
    
    console.log('Server is running on http://localhost:3000')
    
    serve({
      fetch: app.fetch,
      port: 3000,
    })
    

    💡 ポイント
    createRoutezod を使うことで、バリデーションロジックとドキュメント定義を一箇所にまとめています。これが「Single Source of Truth(信頼できる唯一の情報源)」となります。

手順2. フロントエンド構築 (Next.js + Orval)

一度ルートディレクトリに戻り、Next.jsのプロジェクトを作成します。

  • ? Would you like to use React Compiler? … No / Yes が表示された場合、Yesを選んでください。今回はどちらでも良いのですが、このまま本番の実装に入る場合はこちらの方が便利です。 React Compilerについて
  • ? Would you like your code inside a src/ directory? › No / Yes が表示された場合、Yesを選んでください。こちらはsrcディレクトリ作成の有無です。
# backendディレクトリから抜ける
cd ..

# Next.jsプロジェクトの作成 (frontendという名前で作成)
npx create-next-app@latest frontend --typescript --eslint --tailwind  --app --import-alias "@/*"

Success! Created frontend at ...が表示されればOK!

次は、作成されたfrontendディレクトリに移動し、OrvalとReact Queryをセットアップします。

cd frontend

# React QueryとAxios
npm install @tanstack/react-query axios

# Orval
npm install -D orval

手順2.1. 設定ファイルの実装

frontend/orval.config.ts を作成します。 ここが「バックエンドとフロントエンドを繋ぐ架け橋」になります。

import { defineConfig } from 'orval';

export default defineConfig({
  api: {
    // バックエンドのOpenAPI定義URLを指定
    input: 'http://localhost:3000/doc',
    output: {
      mode: 'tags-split', // タグごとにファイルを分割するモード
      // 出力先のベースディレクトリを指定
      // ※ tags-splitモードの場合、このファイル名自体は生成されず、
      // このパスを基準にタグごとのディレクトリ(default/など)が作成されます
      target: './src/gen/api.ts',
      schemas: './src/gen/model',
      client: 'react-query',
      baseUrl: 'http://localhost:3000', // APIのベースURL
      mock: true, 
    },
  },
});

手順2.2. React Queryのセットアップ

生成されたフックを使うために、アプリケーション全体を QueryClientProvider で囲みます。
まず、frontend/src/app/providers.tsx を新規作成します。

'use client'

import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useState } from 'react'

export default function Providers({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient())

  return (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  )
}

次に、frontend/src/app/layout.tsx でこのProviderを読み込みます。

import Providers from "./providers"; // 追加

// ... (中略)

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="ja">
      <body className={inter.className}>
        <Providers>
          {children}
        </Providers>
      </body>
    </html>
  );
}

手順2.3. データ表示の実装

最後に、トップページでデータを取得・表示します。ここで、Orvalが生成したフック useGetUsersId が火を吹きます!
frontend/src/app/page.tsx を以下のように修正します。
※現状ではエラーが出ますが、気にせず次に進んでください!

  'use client' // フックを使うので必須

  import { useGetUsersId } from '@/gen/default/default' // 自動生成されたフック

  export default function Home() {
    // バックエンドの仕様書から生成されたフックを利用
    // ID '123' のユーザーを取得
    const { data, isLoading, error } = useGetUsersId('123')

    if (isLoading) return <div className="p-10">Loading...</div>
    if (error) return <div className="p-10 text-red-500">Error: {error.message}</div>

    return (
      <main className="flex min-h-screen flex-col items-center justify-between p-24">
        <div className="z-10 max-w-5xl w-full font-mono text-sm">
          <h1 className="text-4xl font-bold mb-8">Hono x Orval Demo</h1>

          <div className="border p-6 rounded-lg shadow-md bg-white text-black">
            <h2 className="text-2xl mb-4 font-bold">User Profile</h2>
            <ul className="space-y-2 text-lg text-black">
              <li><strong>ID:</strong> {data?.data?.id || ''}</li>
              <li><strong>Name:</strong> {data?.data?.name || ''}</li>
              <li><strong>Age:</strong> {data?.data?.age || ''}</li>
            </ul>
          </div>
        </div>
      </main>
    )
  }

5. 動作確認

手順1. Swagger UIと「型貫通」の体験

実際にサーバーを起動し、自動生成されたAPI仕様書(Swagger UI) の確認から、画面表示までを行ってみましょう!ターミナルを2つ用意してください。

1. バックエンド起動

ターミナルAでバックエンドサーバーを起動します。

# my-app/backend ディレクトリで実行
npx tsx src/index.ts

ブラウザで http://localhost:3000/ui にアクセスしてみてください。
Swagger UI が表示され、綺麗なAPI仕様書が自動生成されていることが確認できるはずです!
これがコードファーストの力です!
Zodで書いたスキーマ定義が、そのままリッチなドキュメントになっています!
スクリーンショット 2026-02-05 20.47.21.png

2. フロントエンドコード生成

ターミナルBで、フロントエンド側からコード生成コマンドを叩きます。
これが「バックエンドの仕様をフロントエンドに取り込む」瞬間です。

# my-app/frontend ディレクトリで実行
npx orval

成功すると、frontend/src/gen ディレクトリにフックと型定義が生成されます。

3. フロントエンド画面確認

最後に、Next.jsの開発サーバーを起動して画面を確認します。

# my-app/frontend ディレクトリで実行
npm run dev

ブラウザで http://localhost:3001 にアクセスしてください。 画面に "User Profile" と共に、バックエンドから取得した Generated User の情報が表示されていれば成功です!

スクリーンショット 2026-02-05 20.30.31.png


💡 ここがすごい! フロントエンドエンジニアは、Swagger UIを見ながら型を手打ちする必要はありません。 npx orval と叩くだけで、最新の仕様に追従したフックが手に入り、そのままコンポーネントで呼び出すだけでデータが表示されます。

Orvalの堅牢性のテスト(ハイライト)

ここで、バックエンドの UserSchema から age を削除 してみてください。

  1. backend/src/index.tsUserSchema から age を削除してサーバー再起動。
  2. frontend ディレクトリで npx orval を実行。
  3. 結果: frontend/src/app/page.tsx で TypeScriptエラー(ageが存在しない) が発生します!

これが「型が貫通している」状態です。実行時エラーではなく、エディタ上でバグに気づけるのです。実際にここから実装を進めたい方は、修正してください!

6. 参考文献・まとめ

参照ドキュメント

以下の公式リソースを参考にしています。

まとめ

お疲れ様でした!今回の手順で、「バックエンドとフロントエンドが分離した構成でも、型情報を完全に同期させる」 環境を実現することができました。

今回達成したこと

  • HonoとZodで、コードからAPI仕様書を自動生成した。
  • Orvalを使って、仕様書からReact Queryフックを自動生成した。
  • API定義書を書く作業を開発フローから排除した。

※今回作成したプロジェクトのGitHub repository: URL

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?