はじめに
こんにちは、株式会社レコチョクの小林です。 日頃は、主にPythonを用いたバックエンドの開発に携わっています。
最近参画した業務で、バックエンドをPythonのFastAPIで、フロントエンドをNext.jsとTypeScriptで実装する機会があり、それぞれ異なるチームが開発を担当する中で、バックエンドとフロントエンド間での型情報の共有が課題となりました。
そこで本記事では、異なる言語間でどのように型定義を同期させるか、その解決方法を検討します。
開発環境
- バックエンド:Python 3.12.0、FastAPI 0.110.3
- フロントエンド:Next.js 15.2.2、TypeScript 5.8.2
- コード管理:GitHubで別々のリポジトリで管理
- コンテナ管理:Dockerを使用
現状の型定義方法とその課題
前述のプロジェクトでは、バックエンドとフロントエンド間の型共有に関して、主に以下のような方法を採用していました。
- 手動での型定義:フロントエンド開発者がバックエンドのAPIドキュメントを参照し、TypeScriptの型定義を手動で作成
- ドキュメントベースの開発:Swagger UIなどのAPIドキュメントを参照しながら開発を進める
- コミュニケーションベース:バックエンドの変更があった場合、口頭やチャットでフロントエンド開発者に伝達
これらの方法には以下のような課題がありました:
- 手動更新の手間: APIの変更ごとに型定義を手動で更新する必要があり、時間がかかる
- 型定義の不一致: 更新漏れや誤った型定義により、実際のAPIレスポンスと型定義が一致しないケースが発生
- 変更の見落とし: バックエンドの変更がフロントエンド開発者に正確に伝わらないことがある
- 開発の遅延: 型定義の不一致によるバグの発見と修正に時間を要する
これらの課題を解決するために、FastAPIが自動生成するOpenAPI仕様を活用した型共有の方法を検討しました。
なぜ型共有が重要なのか?
FastAPIとTypeScriptの型システム
FastAPIはPythonの型ヒントを活用し、自動的にAPIのバリデーションやドキュメント生成を行います。一方、TypeScriptはJavaScriptに静的型付けを追加した言語で、開発時の型チェックによりバグを早期に発見できます。
両者とも優れた型システムを持っていますが、これらは別々の言語であるため、自動的に型情報が共有されることはありません。この「型の断絶」が様々な問題を引き起こします。
型共有がない場合の問題点
- ランタイムエラーの増加:バックエンドAPIの変更がフロントエンドに正確に伝わらず、実行時にエラーが発生
- 開発効率の低下:フロントエンド開発者がAPIの仕様を確認するために、ドキュメントを参照したり、バックエンド開発者に問い合わせる時間が増加
- ドキュメントの不一致:APIドキュメントが最新の実装と一致しない状況が頻繁に発生
- 型安全性の喪失:FastAPIとTypeScriptそれぞれが提供する型安全性の恩恵が、API通信の境界で失われてしまう
ベースとなるFastAPI実装
今回の検証でベースとなるFastAPIを以下のように実装し、localhost:8101で実行します。
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class User(BaseModel):
id: int
name: str
email: str
@app.get("/users")
def get_users() -> list[User]:
return [
User(id=1, name="John Doe", email="john.doe@example.com"),
User(id=2, name="Jane Smith", email="jane.smith@example.com"),
]
このAPIのレスポンスは以下です。
[
{
"id": 1,
"name": "John Doe",
"email": "john.doe@example.com"
},
{
"id": 2,
"name": "Jane Smith",
"email": "jane.smith@example.com"
}
]
OpenAPI仕様を活用した型共有によるアプローチ
前述の通り、FastAPIは自動的にOpenAPI仕様を生成します。この仕様を活用してTypeScriptの型定義を自動生成する方法が考えられます。
実装手順
1. フロントエンドプロジェクトに必要なパッケージをインストール
今回は、openapi-typescript(v7.6.1)を用いてOpenAPIスキーマからTypeScriptコードを生成します。
npm install --save-dev openapi-typescript@7.6.1
2. 型定義生成+Next.js起動のスクリプト作成
entrypoint.shとして以下を作成。 OpenAPI仕様からのTypeScript型定義生成とNext.jsアプリケーションを起動するスクリプトです。
#!/bin/sh
set -e
# OpenAPI仕様を取得してTypeScript型定義を生成
echo "OpenAPI仕様を取得中..."
curl -s http://localhost:8101/openapi.json > ./openapi.json
echo "TypeScript型定義を生成中..."
mkdir -p ./src/types
npx openapi-typescript openapi.json --output ./src/types/api.ts
echo "API型定義の生成が完了しました"
# Next.jsアプリケーションを起動
echo "Next.jsアプリケーションを起動中..."
npm run dev
3. Dockerでの自動実行設定
FROM node:22.4.0
ENV TZ=Asia/Tokyo
ENV ENV=local
WORKDIR /usr/src/app
COPY ./knowledge_frontend /usr/src/app/
RUN yarn install
COPY ./knowledge_frontend/entrypoint.sh /usr/src/app/
RUN chmod +x /usr/src/app/entrypoint.sh
CMD ["/usr/src/app/entrypoint.sh"]
生成される型定義
ここまでの設定でNext.jsアプリケーションを起動すると、以下のように src/types/api.ts が作成されます。 FastAPIで定義した通り、/users のレスポンスとしてid・name・emailが定義されています。
/**
* This file was auto-generated by openapi-typescript.
* Do not make direct changes to the file.
*/
export interface paths {
"/users": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Get Users */
get: operations["get_users_users_get"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
}
export type webhooks = Record<string, never>;
export interface components {
schemas: {
/** User */
User: {
/** Id */
id: number;
/** Name */
name: string;
/** Email */
email: string;
};
};
responses: never;
parameters: never;
requestBodies: never;
headers: never;
pathItems: never;
}
export type $defs = Record<string, never>;
export interface operations {
get_users_users_get: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["User"][];
};
};
};
};
}
実際の使用方法
src/lib/api.ts で以下のように src/types/api.ts で定義した型定義を利用して getUsers() というAPIを作成します。
import { components } from "../types/api";
type User = components["schemas"]["User"];
export async function getUsers(): Promise<User[]> {
const response = await fetch("http://knowledge_backend:8101/users");
if (!response.ok) {
throw new Error('ユーザー取得に失敗しました');
}
const data: User[] = await response.json();
return data;
}
これを src/app/page.tsx で以下のように呼び出してユーザー情報を出力します。
import { getUsers } from "../lib/api";
export default async function Home() {
const users = await getUsers();
return <div>
<h1>ユーザー一覧</h1>
<ul>
{users.map((user) => <li key={user.id}>{user.name} ({user.email})</li>)}
</ul>
</div>
}
こうすることで、OpenAPI仕様を活用した型共有で以下のようにユーザー情報を表示できることが確認できました。

型共有によるメリット・デメリット
以上の検証結果を踏まえメリット・デメリットをまとめました。
メリット
- FastAPIの型定義から自動的に生成されるため、常に最新の状態を維持できる
- Docker起動時に自動実行されるため、開発者が意識する必要がない
- バックエンドの変更がフロントエンドに自動的に反映されるため、型の不一致によるバグを防止できる
デメリット
- 複雑な型定義に制限がある場合がある
- OpenAPI仕様に含まれない内部型は共有できない
- バックエンドサーバーが起動していないと型定義を生成できない
型定義の拡張
生成された型定義だけでは不十分な場合、以下のように拡張することができます。
import { components } from "../types/api";
type User = components["schemas"]["User"];
// Userを拡張してidを追加
type UserWithId = User & { id: string };
まとめ
FastAPIとNext.js(TypeScript)間の型共有は、OpenAPI仕様を活用することで効率的に実現できることを確認できました。 このアプローチにより、異なるチームが担当する場合でも、フロントエンドとバックエンド間の一貫性を保ちながら開発を進めることができそうです。 ただし、今回の検証では比較的シンプルな型定義を扱いました。より複雑なデータ構造を扱う場合にどのような課題が生じるか、また最適な解決策は何かについては、さらなる検証が必要です。 今後も継続的に学習し、より複雑なユースケースにも対応できる知見を深めていきたいと思います。
参考資料
小林圭一朗