バックエンドとフロントエンドの型定義連携

## バックエンドとフロントエンドの型定義連携：FastAPIとTypeScriptの型安全な開発

### はじめに
現代のWebアプリケーション開発では、バックエンドとフロントエンドの間で一貫した型定義を維持することが重要です。特にFastAPI（Python）とTypeScript（React/Next.js）を組み合わせた開発では、型安全性を両端で確保することで、バグを早期に発見し、開発効率を向上させることができます。

この記事では、実際のプロジェクト経験から学んだ、バックエンドとフロントエンドの型定義の連携方法について解説します。

#### 目次

- バックエンド（FastAPI）での型定義
- フロントエンド（TypeScript）での型定義
- データ型変換の自動処理
- よくあるケースと解決策
- ベストプラクティス

### 1. バックエンド（FastAPI）での型定義
FastAPIはPydanticを使用して、リクエストやレスポンスの型定義を行います。
**スキーマ定義の例（バックエンド側）**

```
from pydantic import BaseModel, Field
from typing import List
from datetime import datetime
from uuid import UUID

# 基本モデル
class TrainingBase(BaseModel):
    menu: str = Field(..., min_length=2, max_length=100)

# 作成用モデル（リクエスト）
class TrainingCreate(TrainingBase):
    pass

# レスポンスモデル
class TrainingResponse(TrainingBase):
    id: UUID
    created_at: datetime

    class Config:
        from_attributes = True  # ORMモデルから変換可能に

# 一覧取得用レスポンスモデル
class TrainingList(BaseModel):
    items: List[TrainingResponse]
    total: int
```

### エンドポイントの定義

```
@router.post(
    "/menu", 
    response_model=TrainingResponse,  # レスポンスの型を指定
    status_code=status.HTTP_201_CREATED
)
def create_training_menu(menu: TrainingCreate, db: Session = Depends(get_db)):
    """新しいトレーニングメニューを作成します"""
    return training_crud.create_training(db, menu)

@router.get("/menu", response_model=TrainingList)
def read_training_menu(db: Session = Depends(get_db)):
    """全てのトレーニングメニュー一覧を取得します"""
    trainings = training_crud.get_all_trainings(db)
    total = training_crud.get_trainings_count(db)
    return {"items": trainings, "total": total}
```

### 2. フロントエンド（TypeScript）での型定義
フロントエンドでは、バックエンドのスキーマに対応するTypeScriptの型定義を作成します。
**型定義の例（フロントエンド側）**

```
// types/AddMenu.ts
export type CreateAddMenu = {
  menu: string
}

export type MenuItemType = {
  id: string  // バックエンドではUUIDだが、フロントではstringとして扱う
  menu: string
  created_at: string  // バックエンドではdatetimeだが、フロントではstringとして扱う
}
```

**APIクライアントの定義**

```
// api/AddMenu/AddMenu.ts
import axios from 'axios'
import { CreateAddMenu, MenuItemType } from '../../types/AddMenu'

const BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080'

export const addMenuApi = {
  // 作成API
  create: async (data: CreateAddMenu) => {
    try {
      const response = await axios.post(`${BASE_URL}/training/menu`, data, {
        headers: {
          'Content-Type': 'application/json',
        },
      })
      console.log('レスポンス', response.data)
      return response.data
    } catch (error) {
      console.error('メニュー追加に失敗しました', error)
    }
  },

  // 一覧取得API
  getAll: async (): Promise<{ items: MenuItemType[]; total: number }> => {
    try {
      const response = await axios.get(`${BASE_URL}/training/menu`)
      return response.data
    } catch (error) {
      console.error('メニュー一覧の取得に失敗しました', error)
      throw error
    }
  },

  // 削除API
  delete: async (id: string) => {
    try {
      await axios.delete(`${BASE_URL}/training/menu/${id}`)
      return true
    } catch (error) {
      console.error('トレーニングメニューの削除に失敗しました', error)
      throw error
    }
  },
}
```

### 3. データ型変換の自動処理
バックエンドとフロントエンドでは、同じデータでも型が異なる場合があります。特に注意が必要なのは以下の型です：
**バックエンド（FastAPI）/ フロントエンド（TypeScript）/ 変換タイミング**
　UUID  　　　　　　　　　　string　　　　　　　　　　　JSON化/パース時
　datetime　　　　　　　　　string　　　　　　　　　　　JSON化/パース時
　Enum　　　　　　　　　　　string/number　　　　　　　JSON化/パース時


### 自動変換のメカニズム

**バックエンド → フロントエンド:**

- FastAPIがレスポンスをJSON形式に変換する際、UUIDや日時は自動的に文字列に変換されます。
- Enumは値（文字列または数値）に変換されます。


**フロントエンド → バックエンド:**

- フロントエンドが文字列のIDを送信します（例："1adaabe2-700e-4ebf-96d2-4185ea139958"）。
- FastAPIのパスパラメータやリクエストボディで適切な型（UUID等）を指定すると、自動的に変換されます

**実際の例**

```
# バックエンド側
@router.delete("/menu/{training_id}")
def delete_training_menu(training_id: UUID, db: Session = Depends(get_db)):
    # ここでtraining_idは既にUUIDオブジェクト
    success = training_crud.delete_training(db, training_id)
    # ...
```
```
// フロントエンド側
delete: async (id: string) => {  // 文字列として扱う
  await axios.delete(`${BASE_URL}/training/menu/${id}`)
}
```

### 4. よくあるケースと解決策
**ケース1: 配列型の定義**
バックエンドがアイテムのリストを返す場合、フロントエンドでも配列型を使用する必要があります。

```
// 誤った定義（単一オブジェクト）
getAll: async (): Promise<{ items: MenuItemType; total: number }>

// 正しい定義（配列）
getAll: async (): Promise<{ items: MenuItemType[]; total: number }>
```

注意点: 配列型を正しく指定しないと、フロントエンドで配列メソッド（map, filter, forEach等）が使えずエラーになります。

**ケース2: 日付の処理**
バックエンドではdatetime型、フロントエンドでは文字列として扱われる日付の変換。

```
// フロントエンドでの日付処理
const formattedDate = new Date(item.created_at).toLocaleDateString('ja-JP')
```

**ケース3: オプションフィールドの扱い**
必須でないフィールドの型定義。
```
// バックエンド側
class Profile(BaseModel):
    name: str
    bio: Optional[str] = None

// フロントエンド側
type Profile = {
  name: string;
  bio?: string;  // オプショナルプロパティ
}
```

### 5. ベストプラクティス
**1. バックエンド・フロントエンド間での型定義の一元管理**

- OpenAPI（Swagger）スキーマからTypeScriptの型を自動生成する

 - openapi-typescript
- orval



**2. 明示的なContent-Typeヘッダー設定**
```
axios.post(`${BASE_URL}/api/endpoint`, data, {
  headers: {
    'Content-Type': 'application/json',
  },
})
```

これにより、データの送受信形式が明確になり、デバッグが容易になります。

**3. 厳格な型チェックの有効化**
tsconfig.jsonで厳格なチェックを有効にする：

```
{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true
  }
}
```

**4. バリデーションの二重実装**
セキュリティ上の理由から、バックエンドとフロントエンドの両方でバリデーションを実装することをお勧めします。

- フロントエンド：ユーザー体験向上のため
- バックエンド：セキュリティ確保のため

### まとめ
FastAPIとTypeScriptを組み合わせた開発では、型安全性を両端で確保することが重要です。適切な型定義により：

- コンパイル/実行前のエラー検出
- コード補完とドキュメント効果
- リファクタリングの安全性

これらのメリットが得られ、結果としてより堅牢で保守しやすいアプリケーションを開発できます。特にUUIDや日時などの特殊な型の取り扱いに注意し、バックエンドとフロントエンドの間での型変換を理解することで、多くの一般的なバグを防ぐことができます。再試行Claudeはインターネットにアクセスできません。提供されたリンクは正確でないか、最新でない可能性があります。YT