この記事では Python + TypeScript のモノレポ構成で型定義を一元管理する仕組みと、その運用方法として考えてみたものを紹介します。最近、AIを活用した開発との相性でモノレポ構成についてよく聞くようになりましたが、モノレポ構成には型共有の点でもメリットがあるとの話があり、試してみました。

概要

  • フロントエンド: React 19 + Vite
  • バックエンド: Django 5 + Django REST Framework + drf-spectacular
  • インフラ: AWS CDK (TypeScript) + App Runner + ECR
  • 型共有: OpenAPI スキーマ → TypeScript 型の自動生成

1. 型生成フロー

まず異なる言語間での型共有の仕組みは下記としました。Django Serializer の型定義をオリジナルの定義元とし、そこから TypeScript 用の型を自動生成します。(出力先として記述しているディレクトリ構造は後述します。)

Django Serializer
↓
drf-spectacular ← Django 拡張ライブラリ
↓
OpenAPI Schema (YAML) → shared/schema.yaml に出力
↓
openapi-typescript ← npm パッケージ
↓
TypeScript Types → frontend/src/types/api.generated.ts に出力

型変換の例

Django Serializer OpenAPI Schema TypeScript
IntegerField() type: integer number
CharField(max_length=200) type: string, maxLength: 200 string
BooleanField() type: boolean boolean
DateTimeField() type: string, format: date-time string

※ OpenAPI Schema には maxLength や format: date-time といった情報も出力されますが、TypeScript では表現できません。ただ、内部で実装したAPIのレスポンス型定義としては十分かと思います。

2. 型共有におけるモノレポのメリット

前述の仕組みで型定義は自動生成されますが、フロントエンドとバックエンドのリポジトリが分かれている場合、手動でファイルをコピーする、GitHub Packages で公開する等、何らかの共有の手間が発生します。モノレポなら型の生成結果をフロントエンドのディレクトリに直接出力できるため、手間なく型を共有できるかと思います。

3. ディレクトリ構成(抜粋)

monorepo_test/
├── .github/
│ └── workflows/
│ ├── generate-types.yml # 型生成ワークフロー
│ └── deploy.yml # デプロイワークフロー
├── frontend/
│ └── src/
│ ├── types/
│ │ └── api.generated.ts # 自動生成された TypeScript 型
│ └── hooks/
│ └── useOrders.ts # API を呼び出すカスタムフック
├── backend/
│ ├── api/
│ │ ├── serializers.py # オリジナルの型定義元
│ │ └── views.py # API エンドポイント
│ └── config/
│ └── urls.py # URL ルーティング
├── shared/
│ └── schema.yaml # OpenAPI スキーマ(自動生成)
└── package.json # 型生成スクリプトを定義

型生成スクリプトは以下のように定義しました。

// package.json
{
"scripts": {
"generate:types": "cd backend && python manage.py spectacular --file ../shared/schema.yaml && cd .. && npx openapi-typescript shared/schema.yaml -o frontend/src/types/api.generated.ts"
}
}

下記を実行しています。
1. python manage.py spectacular drf-spectacular で OpenAPI スキーマを生成
2. npx openapi-typescript OpenAPI スキーマから TypeScript 型を生成

4. 運用フロー

開発では下記の2パターンで運用する想定で考えました。

パターン1: 同時開発

バックエンドとフロントエンドを同時に開発する場合のフローです。

1. バックエンドの Serializer を変更
2. npm run generate:types でローカルで型生成
3. フロントエンドを実装(生成された型を使用)
4. まとめてコミット → PR → マージ → 自動デプロイ

型生成はローカルで行い、生成された型ファイルも一緒にコミットする形になります。

パターン2: バックエンド先行開発

バックエンドとフロントエンドを別々に開発する場合のフローです。(型定義をPythonから生成するためバックエンドを先行して開発する形になります。)

1. バックエンド開発者: API 変更 → PR マージ
2. GitHub Actions が型を自動生成・コミット
3. フロントエンド開発者: 親ブランチを pull → 最新の型を取得
4. フロントエンド開発者: 型を使って実装 → PR マージ → 自動デプロイ

※ フロントエンドのみの変更(型定義に影響しない修正など)は上記のステップ4から開始するイメージです。

5. 実際に API を追加してみる(パターン1: 同時開発)

まずパターン1の運用フローについて、例として Order(注文)エンドポイントを追加する流れで説明します。

Step 1: バックエンドに Serializer と View を追加

まず、Django 側で型定義(Serializer)と API エンドポイント(View)を実装します。

# backend/api/serializers.py
from rest_framework import serializers

class OrderSerializer(serializers.Serializer):
"""注文情報の型定義"""
id = serializers.IntegerField(read_only=True)
product_name = serializers.CharField(max_length=200)
quantity = serializers.IntegerField(min_value=1)
price = serializers.DecimalField(max_digits=10, decimal_places=2)
ordered_at = serializers.DateTimeField(read_only=True)
# backend/api/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from drf_spectacular.utils import extend_schema

class OrderListView(APIView):
@extend_schema(responses={200: OrderSerializer(many=True)})
def get(self, request):
# 実際はDBから取得
orders = [
{"id": 1, "product_name": "テスト商品", "quantity": 2, "price": "10000", "ordered_at": "2025-12-01T10:30:00Z"},
]
return Response(orders)
# backend/config/urls.py
urlpatterns = [
path('api/orders/', OrderListView.as_view(), name='order-list'),
]

Step 2: OpenAPI スキーマを生成

型生成スクリプトを実行します。

$ npm run generate:types

> generate:types
> cd backend && python manage.py spectacular --file ../shared/schema.yaml && cd .. && npx openapi-typescript shared/schema.yaml -o frontend/src/types/api.generated.ts

出力ログ

=== API型生成 ===

✨ openapi-typescript 7.10.1
🚀 shared/schema.yaml → frontend/src/types/api.generated.ts [21.3ms]
出力ファイル:
- shared/schema.yaml
- frontend/src/types/api.generated.ts

Step 3: 生成された OpenAPI スキーマを確認

shared/schema.yaml に Order の定義が追加されています。

# shared/schema.yaml(自動生成)
openapi: 3.0.3
info:
title: Monorepo POC API
version: 1.0.0
paths:
/api/orders/:
get:
operationId: api_orders_list
responses:
'200':
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Order'
components:
schemas:
Order:
type: object
properties:
id:
type: integer
readOnly: true
product_name:
type: string
maxLength: 200
quantity:
type: integer
minimum: 1
price:
type: string
format: decimal
ordered_at:
type: string
format: date-time
readOnly: true
required:
- product_name
- quantity
- price

Step 4: 生成された TypeScript 型を確認

frontend/src/types/api.generated.ts に型が追加されています。

// frontend/src/types/api.generated.ts(自動生成)
export interface components {
schemas: {
Order: {
readonly id: number;
product_name: string;
quantity: number;
price: string;
readonly ordered_at: string;
};
};
}

Step 5: フロントエンドで型を使う

レスポンス型定義として利用します。

// frontend/src/hooks/useOrders.ts
import { components } from '../types/api.generated';

type Order = components['schemas']['Order'];

export function useOrders() {
const [orders, setOrders] = useState<Order[]>([]);

useEffect(() => {
fetch('/api/orders/')
.then(res => res.json())
.then((data: Order[]) => {
setOrders(data);
});
}, []);

return orders;
}

6. GitHub Actions での自動化(パターン2: バックエンド先行開発)

続いてパターン2について説明します。GitHub Actions のフローは変更箇所に応じて下記のような形にしています。

  • backend/** の変更 →(push)→ 型生成 →(workflow_run)→ バックエンドデプロイ
  • frontend/** の変更 →(push)→ フロントエンドデプロイ
  • 両方変更 →(push)→ 型生成 →(workflow_run)→ 両方デプロイ(並列)

6.1 型の自動生成(push イベント)

まず型生成について、バックエンドへの push により CI が自動でフロントエンド用の型を生成してコミットする形になります。(※ 型に変更がない場合は差分がないためコミットされません。)

# .github/workflows/generate-types.yml
name: Generate API Types

on:
push:
paths:
- 'backend/**'

jobs:
generate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

# Python/Node.js セットアップは省略

- name: Generate types
run: ./scripts/generate-types.sh

- name: Commit generated files
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: 'chore: auto-generate API types'
file_pattern: 'shared/*.yaml frontend/src/types/*.ts'

6.2 デプロイ時の変更検知(workflow_run イベント)

続いてデプロイのワークフローですが、型生成の完了後に実行したいため workflow_run で処理完了まで待機しています。また、workflow_run イベントでは標準の paths フィルタが使えないので dorny/paths-filter というサードパーティのアクションを使って フロントエンド/バックエンド どちらが変更されたかを検知し、対応するデプロイジョブを実行する形にしています。

# .github/workflows/deploy.yml(抜粋)
on:
workflow_run:
workflows: ["Generate API Types"]
types: [completed]
# ...

jobs:
detect-changes:
steps:
- uses: dorny/paths-filter@v3
id: changes
with:
filters: |
frontend:
- 'frontend/**'
backend:
- 'backend/**'
outputs:
frontend: ${{ steps.changes.outputs.frontend }}
backend: ${{ steps.changes.outputs.backend }}

deploy-backend:
needs: detect-changes
if: needs.detect-changes.outputs.backend == 'true'
# ...

deploy-frontend:
needs: detect-changes
if: needs.detect-changes.outputs.frontend == 'true'
# ...

自動コミット完了後、フロントエンド開発者は pull して最新の型を取得する流れになります。

まとめ

モノレポ構成で、バックエンドの Serializer から TypeScript 型を自動生成する仕組みができました。フロントエンドとバックエンドの型不整合が軽減され、開発が少し効率的になるように思います。

所感

実際にサンプルを作ってみて、下記のような運用で気になる点も出てきました。

  • パターン1(同時開発)での型生成スクリプトの実行忘れ → CI で差分チェックを入れるべき
  • パターン2(バックエンド先行開発)での pull し忘れによる型の不整合 → PR 段階でビルドチェックを入れるべき

また、スキーマの取得方法について、今回はコマンドでファイル出力する方式を採用しましたが、drf-spectacular はエンドポイント経由でスキーマを取得する方法も提供しているようです。エンドポイント経由であればパターン2の運用がよりシンプルにできたかもしれません。型の自動生成自体は簡単に実現できて良かったものの、フローについてはもう少し検討が必要だと感じました。ただ、スキーマの取得方法に関わらず生成した型をフロントエンドのディレクトリに直接出力できる点はモノレポ構成のメリットだと思いました。