AI時代のスキーマファースト開発 FastAPI × GitHub Packages で型安全なSDKを自動配布する
はじめに
この記事は 3-shake Advent Calendar 2025 の記事です。
フロントエンド開発者とバックエンド開発者の間で「APIの仕様が違う」「ドキュメントが古い」といった問題に悩まされたことはありませんか?
さらにAI時代になり、Claude CodeやCursorなどのAIコーディングツールを使う機会が増えてきました。しかし、AIにAPI呼び出しを実装させると、存在しないエンドポイントを「想像」で実装してしまったり、パラメータの型を間違えたりすることがあります。
本記事では、FastAPIのOpenAPI自動生成機能を活用し、GitHub ActionsでTypeScript SDKを自動生成・GitHub Packagesで配布するアーキテクチャを紹介します。この仕組みは、単なる開発効率化だけでなく、**AI時代における「ガードレール」**としても機能すると考えてこの記事を書きました。
このアーキテクチャで解決できること
- API仕様の Single Source of Truth: バックエンドのコードが唯一の仕様書
- 型安全なAPI呼び出し: TypeScriptの型補完でパラメータミスを防止
- 自動ドキュメント: Pythonのdocstringがそのままクライアントのドキュメントに
-
AIのガードレール:
node_modulesとして配布することで、AIが勝手に変更しない境界を作る
全体アーキテクチャ
実装ステップ
Step 1: FastAPIでOpenAPI仕様を生成する
FastAPIは標準でOpenAPI 3.0仕様を自動生成します。
このとき重要になるのが operation_id です。生成されるSDKのメソッド名はこの operation_id に基づいて決定されます。関数名に依存するとリファクタリング時に意図せずSDKのメソッド名が変わってしまう(破壊的変更になる)ため、FastAPIのデコレータで明示的に指定することを推奨します。
# operation_id="getUsers" と指定することで、
# Pythonの関数名を変えてもSDKのメソッド名は "getUsers" に固定される
@router.get("/users", operation_id="getUsers")
async def get_users_handler():
pass
次に、CI上でOpenAPI仕様をJSONファイルとして出力するためのスクリプトを作成します。
※Github Actions内でopenapi.jsonを生成せずともHaskyなどでコミット前にrouterなどに変更があった場合、openapi.jsonを生成するというルールでも良いかもしれないです
#!/usr/bin/env python3
"""
FastAPIからOpenAPI仕様を生成してSDK用に出力するスクリプト
"""
import json
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
def generate_openapi():
"""OpenAPI仕様をJSONファイルとして出力"""
from app.main import app
openapi_data = app.openapi()
openapi_data["info"]["title"] = "Your API Title"
output_path = "client-sdk/openapi.json"
os.makedirs(os.path.dirname(output_path), exist_ok=True)
with open(output_path, "w", encoding="utf-8") as f:
json.dump(openapi_data, f, indent=2, ensure_ascii=False)
print(f"✅ OpenAPI spec generated: {output_path}")
if __name__ == "__main__":
generate_openapi()
ポイント: operation_id を明示することで、バックエンドの実装詳細(関数名)とSDKのインターフェース(メソッド名)を分離できます。
# Python側
@router.get("/users", operation_id="getUsers")
async def implementation_detail_name(): ...
# 生成されるSDK側
getUsers()
Step 2: client-sdkディレクトリの構成
client-sdk/
├── package.json
├── tsconfig.json
├── openapi.json # 自動生成
└── src/
├── index.ts # エントリーポイント ※自分でカスタマイズしても良い
└── generated/ # 自動生成されるコード
├── services.gen.ts
├── types.gen.ts
└── core/
package.json:
{
"name": "@your-scope/your-client-sdk",
"version": "0.1.0",
"description": "TypeScript Client SDK for Your API",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist"],
"scripts": {
"generate": "npx @hey-api/openapi-ts -i ./openapi.json -o ./src/generated -c fetch",
"build": "tsc",
"prepublishOnly": "npm run build"
},
"publishConfig": {
"registry": "https://npm.pkg.github.com",
"access": "restricted"
},
"devDependencies": {
"@hey-api/openapi-ts": "^0.48.0",
"typescript": "^5.3.0"
}
}
Step 3: GitHub Actions ワークフロー
name: Generate and Publish Client SDK
on:
push:
branches: [main, develop]
paths:
- "app/routers/**/*.py" # APIの変更時のみトリガー
workflow_dispatch:
jobs:
generate-sdk:
runs-on: ubuntu-latest
permissions:
contents: write
packages: write
steps:
- uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
# Python環境セットアップ
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: "3.12"
- name: Install Python dependencies
run: pip install fastapi uvicorn sqlmodel # 必要な依存関係
# Node.js環境セットアップ
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
registry-url: "https://npm.pkg.github.com"
scope: "@your-scope"
# OpenAPI仕様生成
- name: Generate OpenAPI spec
run: python tools/open-api-generate/main.py
# TypeScript SDK生成
- name: Generate TypeScript SDK
working-directory: ./client-sdk
run: |
npm ci
npm run generate
# 変更チェック
- name: Check for SDK changes
id: check_changes
working-directory: ./client-sdk
run: |
git diff --exit-code src/generated || echo "has_changes=true" >> $GITHUB_OUTPUT
# バージョンアップ & 公開
- name: Version and publish SDK
if: steps.check_changes.outputs.has_changes == 'true'
working-directory: ./client-sdk
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
npm version patch --no-git-tag-version
npm run build
npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# 変更をコミット(無限ループ防止)
- name: Commit SDK changes
if: steps.check_changes.outputs.has_changes == 'true'
run: |
git add client-sdk/
BRANCH_NAME=$(echo ${GITHUB_REF#refs/heads/})
git commit -m "chore: update generated client SDK [skip ci]"
git pull --rebase origin $BRANCH_NAME
git push origin $BRANCH_NAME
重要なポイント:
-
パストリガー:
app/routers/**/*.pyの変更時のみ実行 -
[skip ci]: コミットメッセージに含めることで無限ループを防止 - リベース: 並行実行時のコンフリクトを防止
Step 4: フロントエンドでの利用
# .npmrcの設定(GitHub Packages認証)
echo "@your-scope:registry=https://npm.pkg.github.com" >> .npmrc
echo "//npm.pkg.github.com/:_authToken=YOUR_TOKEN" >> .npmrc
# インストール
npm install @your-scope/your-client-sdk
import { getUsers, createUser, OpenAPI } from '@your-scope/your-client-sdk';
// 認証トークンの設定
OpenAPI.TOKEN = 'your-jwt-token';
OpenAPI.BASE = 'https://api.example.com';
// 型安全なAPI呼び出し
const response = await getUsers({
page: 1,
limit: 10,
sortBy: 'created_at',
sortOrder: 'desc'
});
// response.items は User[] として型推論される
console.log(response.items);
AI時代の「ガードレール」としてのnode_modules
ここからが本記事の伝えたいことです。
なぜnode_modulesがガードレールになりえるのか
AIコーディングツール(Claude Code、Cursor、GitHub Copilotなど)は、大量のコードベースから学習しています。
node_modulesとしてインストールされたライブラリをAIが回収するという挙動は基本的にしないと考えて良いと思っています。
ここは自分の経験則なので、確証があるわけでは無いです・・。
これを利用すると:
- SDKとして提供されたAPI呼び出しロジックを、AIが勝手に書き換えない
- 存在しないエンドポイントを「想像」で実装しない
- 型定義を見て、正しいパラメータで呼び出す
具体例:AIとのペアプログラミング
SDKがない場合のAIの挙動:
// AIに「ユーザー一覧を取得して」と依頼すると...
const response = await fetch('/api/users'); // エンドポイントが正しいかわからない
const users = await response.json(); // 型がanyになる
AIは良かれと思って「それっぽい」エンドポイントを推測してしまいます。
SDKがある場合のAIの挙動:
// AIがnode_modulesのSDKを参照して実装
import { getUsers } from '@your-scope/your-client-sdk';
const response = await getUsers({ page: 1, limit: 10 });
// response は GetUsersResponse 型として推論される
AIはSDKの型定義を見て:
- 存在するエンドポイントのみを使用
- 必須パラメータを正しく指定
- レスポンスの型を正確に把握
SDKは「契約(Contract)」として機能する
生成されるSDKの具体例
services.gen.ts(メソッド定義)
/**
* ユーザー一覧取得
* ユーザー一覧を取得します(管理者認証必須)
* @param data The data for the request.
* @param data.page ページ番号
* @param data.limit 1ページあたりの件数
* @param data.loginId ログインIDでのフィルタリング(あいまい検索)
* @returns UserListResponse Successful Response
* @throws ApiError
*/
export const getUsers = (data: GetUsersData = {}): CancelablePromise<GetUsersResponse> => {
return __request(OpenAPI, {
method: 'GET',
url: '/api/admin/users',
query: {
page: data.page,
limit: data.limit,
login_id: data.loginId,
login_id: data.loginId,
},
errors: {
401: '認証エラー',
422: 'Validation Error',
}
});
};
注目ポイント:
- FastAPIのdocstringがJSDocとしてそのまま反映
- 日本語のエラーメッセージも保持
- パラメータの説明も自動生成
types.gen.ts(型定義)
export type GetUsersData = {
page?: number;
limit?: number;
loginId?: string;
companyId?: number;
isAdmin?: boolean;
sortBy?: string;
sortOrder?: string;
// ...
};
export type GetUsersResponse = UserListResponse;
export type UserListResponse = {
items: User[];
total: number;
page: number;
limit: number;
};
導入時のTips
1. operation_idの命名規則を統一する
# Good: 動詞 + 名詞で一貫性を保つ
@router.get("/users", operation_id="getUsers")
async def get_users(): pass
@router.post("/users", operation_id="createUser")
async def create_user(): pass
@router.get("/users/{user_id}", operation_id="getUserById")
async def get_user_by_id(): pass
2. エラーレスポンスも型定義する
raise HTTPException するだけではOpenAPI仕様には反映されません。デコレータの responses 引数を使って明示的に定義することで、SDKにもエラー情報が出力されます。
@router.get(
"/users/{user_id}",
operation_id="getUserById",
responses={
404: {"description": "ユーザーが見つかりません"}
}
)
async def get_user_by_id(user_id: int):
user = await get_user(user_id)
if not user:
raise HTTPException(status_code=404, detail="ユーザーが見つかりません")
return user
3. Pydanticモデルにdescriptionを追加
from pydantic import BaseModel, Field
class UserCreate(BaseModel):
login_id: str = Field(..., description="ログインID(メールアドレス形式)")
password: str = Field(..., description="パスワード(8文字以上)")
is_admin: bool = Field(False, description="管理者権限を付与するか")
まとめ
本記事で紹介したアーキテクチャの利点をまとめます:
| 観点 | メリット |
|---|---|
| 開発効率 | API変更が自動でフロントに反映 |
| 型安全性 | TypeScriptの型補完でミスを防止 |
| ドキュメント | Pythonコメントがそのままドキュメントに |
| AI協調 | node_modulesとして「触らない境界」を作る |
| チーム開発 | 仕様の齟齬が発生しない |
特にAI時代においては、「AIが変更して良い領域」と「変更すべきでない領域」を明確に分けることが重要です。SDKをnode_modulesとして配布することで、この境界を自然に作ることができます。
ぜひこのアーキテクチャを導入して、型安全で効率的なAPI開発を実現してください。
この記事がfastapiを使っていて、クライアントサイドの人とのコミュニケーションコストやスキーマをどうあわせるかで迷っている人の参考になれば幸いです。
ありがとうございました。
Discussion