■はじめに
こんにちは!
iimonでエンジニアをしています「しらみず」です。
現在、新規サービス開発をする中で、フロント側とサーバー側のリクエストとレスポンスの型やリクエストパスなどに差が生じないように、どうやってAPIクライアントを生成するかが課題でした。
手動でAPIを叩くコードをフロント側で書くとなると、以下のような課題が発生します。
- バックエンドのリクエストパスとレスポンスに合わせて、フロントで型定義などを手書きする
fetch('/api/todos')のようなfetchラッパーを毎回自分で書く- バックエンドのスキーマが変わったとき、フロントの型定義を手動で直す
どれも地味に手間がかかります。さらに「バックエンドの定義と本当に合ってる?」という不安がずっとついてまわります。
この記事では、それを解決するツール Orval を紹介します。
■環境
バックエンド
- Python >= 3.14
- Django >= 6.0.4
- django-ninja >= 1.6.2
フロントエンド
- React 19.x
- Vite 8.x
- TypeScript 6.x
- Orval 8.x
Pythonのパッケージ管理ツール
- uv
■Orvalとは何か
Orval は、OpenAPIスキーマからTypeScriptの型定義・APIクライアントを自動生成するライブラリです。
OpenAPIスキーマ(JSON/YAML)
↓ Orvalのコマンドを実行すると、APIクライアントを全部自動生成する
・型定義(TypeScript)
・fetchラッパー関数
重要なのは「バックエンド側の定義がSSOT (Single Source of Truth:唯一正しい情報の源泉)になる」というものです。
フロントで型を手書きするのではなく、バックエンドが出力したOpenAPIスキーマを唯一正しい情報の源泉として、そこからフロントのコードを自動生成します。
バックエンドの型が変わったらコマンドを再実行するだけで、フロントの型も追従します。
そのおかげで、最初に挙げた以下の課題を解決することができます。
- バックエンドのリクエストパスとレスポンスに合わせて、フロントで型定義などを手書きする
fetch('/api/todos')のようなfetchラッパーを毎回自分で書く- バックエンドのスキーマが変わったとき、フロントの型定義を手動で直す
■Orvalを使うと何が変わるか
◆手書きの場合
// 型定義を手書き type TodoType = { id: number title: string done: boolean } // fetchラッパーを手書き const getTodos = (): Promise<TodoType[]> => fetch('/api/todos').then((res) => res.json())
Todo一覧を取得するAPIを例に考えてみます。
手書きの場合は、APIが増えるたびに、型定義とfetchラッパーを毎回書くことになります。 バックエンドにフィールドが追加されたら、手動で修正する必要があります。
◆Orvalを使う場合
// 自動生成されたコード(src/api/todos.ts) export const listTodos = (): Promise<TodoOut[]> => customInstance('/api/todos') export const getTodo = (todoId: number): Promise<TodoOut> => customInstance(`/api/todos/${todoId}`)
Orvalを使うと、npx orvalというコマンドを実行することで、型定義とfetchラッパーをOpenAPIスキーマから自動生成してくれます。
そのため、コンポーネント側では、Orvalが生成した関数を呼び出すだけで、型補完が効きながら、APIを叩くコードも書かないで済みます。
もし、テーブルが変わって、フィールドが追加されたら、バックエンドでOpenAPIスキーマを更新して、npx orval を再実行するだけで済みます。
■ハンズオン
◆今回作るもの
monorepo/ ├── backend/ # Django + Django Ninja(uv) └── frontend/ # React + Vite + Orval
今回は、モノレポ構成で以下を使います。
- バックエンド:Django + Django Ninja(uv)
- フロントエンド:React + Vite + Orval
APIは、以下のようにシンプルにします。
GET /api/todos:Todo の一覧を返すGET /api/todos/{id}:Todo を1件返す
◆ディレクトリ構成を作る
mkdir orval-ninja-demo && cd orval-ninja-demo mkdir backend frontend
まずは、作業ディレクトリを作成します。
モノレポなので、backendとfrontendの2つを同じディレクトリ配下に配置します。
◆バックエンドのセットアップ
cd backend uv init uv add django django-ninja
今回は、uvというPythonのパッケージマネージャーを使っています。
一言で言うと「pip + venv + pyenv をまとめて速くしたもの」です。
uvを使うとpipと違って、以下のようなメリットがあります。
- 仮想環境を作る必要がない
- 仮想環境を有効化する必要がない
- Pythonのバージョンやライブラリのバージョンをlockファイルで固定できる
◆Djangoプロジェクトを作成する
uv run django-admin startproject config . uv run python manage.py startapp api
uvを使って、apiアプリケーションとconfigプロジェクトを作成します。
INSTALLED_APPS = [
...
'ninja',
'api',
]
backend/config/settings.pyに'ninja'と'api'アプリケーションを追加します。
◆モデルを定義する
from django.db import models class Todo(models.Model): title = models.CharField(max_length=200) done = models.BooleanField(default=False) def __str__(self): return self.title
api/models.py にTodoモデルを定義します。
uv run python manage.py makemigrations uv run python manage.py migrate uv run python manage.py showmigrations
マイグレーションを作成・適用します。
showmigrations で適用済みか確認できます。
◆スキーマを ModelSchema で定義する
from ninja import ModelSchema from .models import Todo class TodoOut(ModelSchema): id: int title: str done: bool class Meta: model = Todo fields = ["id", "title", "done"]
api/schemas.py を作成します。
ModelSchema を使うと、モデルのフィールド定義からスキーマが自動生成されます。
fields に使いたいフィールド名を指定します。
ModelSchema はデフォルト値があるフィールドをoptional、主キーをnullableとして出力するため、レスポンス用スキーマでは必要に応じてフィールドを明示的に上書きしています。
uv run python manage.py shell
from api.models import Todo Todo.objects.create(title="牛乳を買う", done=False) Todo.objects.create(title="記事を書く", done=True) Todo.objects.create(title="Orvalを試す", done=False) exit()
テーブルにダミーデータを投入しておきます。
◆APIを定義する
from ninja import NinjaAPI from .models import Todo from .schemas import TodoOut api = NinjaAPI() @api.get("/todos", response=list[TodoOut]) def list_todos(request): return Todo.objects.all() @api.get("/todos/{todo_id}", response=TodoOut) def get_todo(request, todo_id: int): todo = Todo.objects.filter(id=todo_id).first() if todo is None: return api.create_response(request, {"detail": "Not found"}, status=404) return todo
api/views.py にAPIを追加します。
from django.contrib import admin from django.urls import path from api.views import api urlpatterns = [ path('admin/', admin.site.urls), path('api/', api.urls), ]
プロジェクトのconfig/urls.py にルーティングを追加します。
◆動作確認とスキーマのファイル出力
uv run python manage.py runserver
サーバーを起動します。
http://localhost:8000/api/docsにアクセスすると Swagger UI が表示されます。
uv run python manage.py export_openapi_schema | uv run python -m json.tool > openapi.json
OpenAPI スキーマをファイルに出力します。
backend/openapi.jsonにファイルが出力されます。
OpenAPI スキーマの JSON はhttp://localhost:8000/api/openapi.jsonでも確認できます。
このファイルを Orval が参照して、APIクライアントを生成します。
◆フロントエンドのセットアップ
cd frontend/ npm create vite@latest . -- --template react-ts npm install npm install -D orval
フロントエンドのセットアップを行って、Orvalをインストールします。
◆Orvalの設定ファイルを書く
import { defineConfig } from 'orval'; export default defineConfig({ todos: { input: { target: '../backend/openapi.json', // バックエンドが出力したOpenAPIスキーマのパス }, output: { mode: 'split', // 型定義ファイルとfetch関数ファイルを分けて生成する target: 'src/api/todos.ts', // fetch関数の出力先ファイル schemas: 'src/api/model', // 型定義の出力先ディレクトリ client: 'fetch', // 使用するHTTPクライアント clean: true, // 再生成時に古いファイルを自動削除 override: { useTypeOverInterfaces: true, // interface → type }, }, }, });
frontend/orval.config.ts を作成します。
input.target にバックエンドで出力したOpenAPIのファイルのパスを指定します。
APIクライアントを生成するには、frontend ディレクトリでnpx orvalを実行します。
src/api/ ├── model/ │ ├── todoOut.ts # 型定義(自動生成) │ └── index.ts └── todos.ts # fetch関数(自動生成)
npx orvalを実行すると、OpenAPIスキーマから、型定義やfetch関数を自動生成してくれます。
自動生成されたfetch関数を使うことで、手動で書くことなくバックエンドとフロントエンドで同期させることができます。
◆モデルのフィールド変更に追従する
class Todo(models.Model): title = models.CharField(max_length=200) done = models.BooleanField(default=False) description = models.TextField(blank=True, default="")
api/models.py に description フィールドを追加します。
uv run python manage.py makemigrations uv run python manage.py migrate
マイグレーションを作成して、実行します。
class TodoOut(ModelSchema): id: int title: str done: bool description: str class Meta: model = Todo fields = ["id", "title", "done", "description"]
api/schemas.py に "description" を追加します。
uv run python manage.py export_openapi_schema | uv run python -m json.tool > openapi.jsonでOpenAPIスキーマを再生成します。
npx orvalでフロント側のAPIクライアントを再生成します。
そうすると"description" がクライアント側に追加されていることがわかります。
これがOrvalの良さで、モデルにフィールドを追加するだけで、TypeScriptの型まで自動で追従します。
■まとめ
今回、Orvalを使うことで、以下の課題を解決することができました。
- バックエンドのリクエストパスとレスポンスに合わせて、フロントで型定義などを手書きする
fetch('/api/todos')のようなfetchラッパーを毎回自分で書く- バックエンドのスキーマが変わったとき、フロントの型定義を手動で直す
ここから更に、以下の点をCIで組み込んで、差分が出ればCIを落とすようにすることで、フロントとサーバーで噛み合ってない状態でリリースしちゃうことを防ぐことができると思います。
uv run python manage.py export_openapi_schema | uv run python -m json.tool > openapi.jsonコマンドを実行して、差分が出たらCIを落とすnpx orvalで差分が出たらCIを落とす
また、AI時代においてNotionにドキュメントがあったり、AIに読み込ませるドキュメントがあったりと何が正しいものなのかわからなくなりますが、OpenAPIスキーマをSSOTとして開発していくことは必要不可欠なのかなーと感じています。
ここまで読んでくださりありがとうございます!
弊社ではエンジニアを募集しております! 少しでもご興味がありましたら、カジュアル面談でお話ししましょう!