🎯 この記事の目的
「FastAPI を触り始めたけれど、各ファイルの役割が分からない…」
そんな FastAPI 初心者の自分がChatGPTやCopilotとファイル上で会話しながら
この記事では「Todo CRUD API」を例に、
- FastAPI の実務的なディレクトリ構成
- ファイルごとの役割
- モデル(SQLAlchemy)
- Pydantic スキーマ
- CRUD の責務分離
- 依存性注入(Depends)
- Router で API を分割するメリット
- lifespan で DB 初期化する理由
などを 図解するように丁寧に説明します。
📁 全体構成(実務向き FastAPI)
FastAPI は小規模でもフォルダを分けた設計が必要です。
backend/
├ src/
│ ├ main.py
│ ├ models.py
│ ├ schemas/
│ │ └ todos.py
│ ├ crud/
│ │ └ todos.py
│ ├ service/
│ │ └ service.py(今回は作成せず)
│ ├ routers/
│ │ └ todos.py
│ ├ database/
│ │ └ database.py
│ ├ dependencies.py
│ └ __init__.py
└ ...
🎓 今回のディレクトリ構成
| 役割 | 説明 |
|---|---|
| main.py | アプリのエントリポイント。CORS や lifespan、router 登録を行う。 |
| models.py | SQLAlchemy を使った DB テーブル定義(ORM モデル) |
| schemas/ | Pydantic モデル。API の入出力の型定義(バリデーション) |
| crud/ | DB 操作を行うロジック(取得/追加/更新/削除) |
| services/ | crud操作に関係のない関数を記述したファイルを格納 |
| routers/ | API の URL ルーティング(HTTP エンドポイント) |
| dependencies.py | DB セッションなどの DI(依存性注入) |
ディレクトリ構成については、こちらの記事を参考にさせていただきました。
① main.py(FastAPI アプリの本体)
🔹 main.py は「アプリの入口」
- FastAPI インスタンスを作る
- CORS 設定を行う
- lifespan イベントでアプリ起動時の処理
- ルーターの登録
FastAPI アプリの基盤になります。
import os
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager
from src.database import Base, engine
from src.routers import todos_router, liveness_router
# ① lifespan コンテキストを定義
@asynccontextmanager
async def lifespan(app: FastAPI):
# アプリ起動時に一度だけ実行したい処理
# マイグレーションツール導入後削除
Base.metadata.create_all(bind=engine)
# ここでDBのテーブルが作られる
yield
# アプリ終了時に実行したい処理があればここに書く(今は何もなし)
# ② FastAPI インスタンスを lifespan 付きで作成
app = FastAPI(lifespan=lifespan)
# CORS設定
cors_origins_env = os.getenv("CORS_ORIGINS", "")
if cors_origins_env == "*" or cors_origins_env == "":
origins = ["*"]
else:
origins = [
o.strip()
for o in cors_origins_env.split(",")
if o.strip()
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins, # 許可するオリジン
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# ルーター登録
app.include_router(todos_router)
app.include_router(liveness_router)
② モデル(models.py)
🔹 これは “DB の構造” を表すファイル
FastAPI は ORM を内蔵していません。
そのため、DB とやり取りするには SQLAlchemy(ORM) を使います。
SQLAlchemy の Model は:
- データベースのテーブル構造(id/int/title など)
- 型
- 文字列長
- default 値
などを定義するファイルです。
from sqlalchemy import Column, Integer, String, Boolean, DateTime, func
from src.database import Base
class Todo(Base):
__tablename__ = "todos" # DB上のテーブル名
id = Column(Integer, primary_key=True, index=True) # 主キー、インデックス付き(インデックスとは検索を高速化するための仕組み)
title = Column(String(255), nullable=False) # タイトル、NULL不可
done = Column(Boolean, default=False, nullable=False) # 完了フラグ、デフォルトはFalse、NULL不可
# 追加
created_at = Column(
DateTime(timezone=True),
server_default=func.now(), # 既存行には現在時刻が入る
nullable=False,
)
updated_at = Column(
DateTime(timezone=True),
server_default=func.now(),
onupdate=func.now(), # UPDATE時に自動で更新される。
nullable=False,
)
③ Pydantic スキーマ(schemas/)
🔹 FastAPI の最大のメリットは「型に基づいた自動バリデーション」
Pydantic の役割は:
- リクエストの型チェック
- レスポンスの型変換
- Swagger(OpenAPI)への自動反映
- セキュリティ(不正入力の排除)
であり、実務では非常に重要です。
スキーマは Model と違うの?
| SQLAlchemy(Model) | Pydantic(Schema) |
|---|---|
| DB のテーブル | API の入出力 |
| 永続データ | バリデーション用 |
| ORM 管理 | 自動型変換 |
役割が違うため、ファイルを分ける必要があるのです。
from pydantic import BaseModel
# スキーマとは、データの構造を定義するもの
# モデルとの違いは、モデルはDBの構造を定義し、スキーマはAPIのリクエストやレスポンスのデータ構造を定義する点にある
class TodoBase(BaseModel):
"""Todoの基本スキーマ"""
title: str # タイトル
done: bool = False # 完了フラグ、デフォルトはFalse
class TodoCreate(TodoBase):
"""POST /todos Todo作成用スキーマ"""
pass # TodoBaseを継承しているので、追加のフィールドは不要
class TodoUpdate(TodoBase):
"""PUT /todos/{id}用スキーマ"""
title: str | None = None # タイトル、更新時に省略可能
done: bool | None = None # 完了フラグ、更新時に省略可能
# デリートはIDだけで良いのでスキーマは不要。もし論理削除なら?、その場合は専用のスキーマを作成することもある
# レスポンス用スキーマは何をしているの?
# レスポンス用スキーマは、APIエンドポイントから返されるデータの構造を定義します。これにより、クライアントが受け取るデータの形式が明確になります。
class Todo(TodoBase):
"""レスポンス用:DBから読み出したデータにidが付いたもの"""
id: int # IDフィールドを追加
class Config:
orm_mode = True # ORMモデルからのデータ変換を許可
④ CRUD(crud/)
🔹 FastAPI に “ビジネスロジックを書かない” のがポイント
FastAPI の router はあくまで「HTTP の窓口」です。
- DB のロジック
- 計算
- バリデーション
- 複雑な処理
などを router に書くと、
プロジェクトが破綻します。
そこで、DB 操作は CRUD モジュールに分離します。
CRUD の責務
| 関数名 | 役割 |
|---|---|
| get_todos | 一覧取得 |
| create_todo | 新規作成 |
| update_todo | 更新 |
| delete_todo | 削除 |
from sqlalchemy.orm import Session
from src import models
from src.schemas import TodoCreate, TodoUpdate
# ここではスキーマではなくてモデルなの?
# はい、そうです。CRUD操作では、データベースと直接やり取りするためにORMモデルを使用します。スキーマは主にAPIのリクエストやレスポンスのバリデーションとシリアライゼーションに使用されます。
def get_todos(db:Session):
"""すべてのTodoを取得"""
# ここではスキーマではなくてモデルなの?
# はい、そうです。CRUD操作では、データベースと直接やり取りするためにORMモデルを使用します。スキーマは主にAPIのリクエストやレスポンスのバリデーションとシリアライゼーションに使用されます。
return db.query(models.Todo).all()
def get_todo(db:Session, todo_id:int):
"""IDでTodoを取得"""
return db.query(models.Todo).filter(models.Todo.id == todo_id).first()
def create_todo(db:Session, todo:TodoCreate):
"""Todoを作成"""
# ここではスキーマも使うの?
# はい、create_todo関数では、引数として受け取ったスキーマインスタンス(TodoCreate)を使用して、新しいTodoモデルインスタンスを作成します。スキーマは、APIから受け取ったデータのバリデーションと構造化に役立ちます。
# GET系では使ってなかったよね?
# その通りです。GET系の関数では、データベースからデータを取得するだけなので、スキーマは必要ありません。一方、データの作成や更新では、スキーマを使用して入力データのバリデーションを行います。
# GET系でもスキーマ使うことあるの?
# 通常、GET系の関数ではスキーマは使用しませんが、レスポンスとして返すデータをスキーマでシリアライズする場合には使用することがあります。
# レスポンス用スキーマのTodoはいつ使うの?
# レスポンス用スキーマのTodoは、APIエンドポイントでデータを返す際に使用します。
# 例えば、FastAPIのエンドポイントで、データベースから取得したTodoモデルインスタンスをレスポンスとして返すときに、このスキーマを使ってデータをシリアライズします。
todo = models.Todo(title=todo.title, done=todo.done)
db.add(todo)
db.commit() # 変更をデータベースに反映
db.refresh(todo) # 作成後の状態を取得
return todo
def update_todo(db:Session, todo_id:int, todo_in:TodoUpdate):
"""Todoを更新"""
todo = db.query(models.Todo).filter(models.Todo.id == todo_id).first()
if not todo:
return None
data = todo_in.dict(exclude_unset=True) # 未指定の項目は無視
for k, v in data.items():
setattr(todo, k, v) # setattrはオブジェクトの属性を動的に設定する組み込み関数
db.commit() # 変更をデータベースに反映
db.refresh(todo) # 更新後の状態を取得
return todo
def delete_todo(db:Session, todo_id:int):
"""Todoを削除"""
todo = db.query(models.Todo).filter(models.Todo.id == todo_id).first()
if not todo:
return None
db.delete(todo)
db.commit() # 変更をデータベースに反映
# 戻り値は{id}のToDoを削除しました
return {"message": f"{todo_id}のToDoを削除しました。"}
⑤ ルーター(routers/)
🔹 FastAPI の “実務での正しい書き方”
FastAPI で最も重要なのが APIRouter です。
例:
router = APIRouter(prefix="/todos", tags=["todos"])
これで
-
/todos/*の API を 1 ファイルにまとめる - Swagger で「todos」タグがまとまって見える
- main.py がスッキリする
というメリットが得られます。
ルーターの責務:
| 責務 | 説明 |
|---|---|
| URL の分離 |
/todos 系を一箇所にまとめる |
| DI で DB session を受け取る | Depends(get_db) |
| CRUD に処理を委譲する | router は薄く保つ |
| リクエスト&レスポンスの型定義 | schemas を使う |
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from src.dependencies import get_db
from src.schemas import Todo, TodoCreate, TodoUpdate
from src import crud
# Routerの作成
# prefixはルーターの共通パス、tagsはドキュメントでの分類に使用
router = APIRouter(
prefix="/todos",
tags=["todos"],
)
# すべてのTodoを取得するエンドポイント
# path="/"は"/todos/"に対応
@router.get("/", response_model=list[Todo])
def list_todos(db: Session = Depends(get_db)):
"""すべてのTodoを取得"""
todos = crud.get_todos(db)
return todos
# IDでTodoを取得するエンドポイント
# path="/{todo_id}"は"/todos/{todo_id}"に対応
@router.get("/{todo_id}", response_model=Todo)
def get_todo(todo_id: int, db: Session = Depends(get_db)):
"""IDでTodoを取得"""
todo = crud.get_todo(db, todo_id)
if not todo:
raise HTTPException(status_code=404, detail="Todo not found")
return todo
# Todoを作成するエンドポイント
# path="/"は"/todos/"に対応
# response_model=Todoは作成後のTodoを返すことを示す
@router.post("/", response_model=Todo, status_code=201)
def create_todo(todo_in: TodoCreate, db: Session = Depends(get_db)):
"""Todoを作成"""
todo = crud.create_todo(db, todo_in)
return todo
# IDでTodoを更新するエンドポイント
# path="/{todo_id}"は"/todos/{todo_id}"に対応
@router.put("/{todo_id}", response_model=Todo)
def update_todo(todo_id: int, todo_in: TodoUpdate, db: Session = Depends(get_db)):
todo = crud.update_todo(db, todo_id, todo_in)
if not todo:
raise HTTPException(status_code=404, detail="Todo not found")
return todo
# Todoを削除するエンドポイント
@router.delete("/{todo_id}")
def delete_todo(todo_id: int, db: Session=Depends(get_db)):
todo = crud.delete_todo(db, todo_id)
if not todo:
raise HTTPException(status_code=404, detail="Todo not found")
return todo
⑥ 依存性注入(dependencies.py)
🔹 FastAPI の DI(Depends)は高機能で便利
特に DB セッションのライフサイクル管理に使います。
DB セッションの基礎:
- API リクエスト毎に 1 つのセッションを生成
- リクエストが終わったら必ず close
- 例外が起きても閉じる
FastAPI の DI では以下のように書きます:
from sqlalchemy.orm import Session
from src.database import SessionLocal
# 下記は何をしているの?
# これはFastAPIの依存関係注入システムで使用される関数です。get_db関数はデータベースセッションを生成し、使用後に適切に閉じるためのものです。
# これにより、各リクエストごとに新しいデータベースセッションが提供され、リソースの管理が容易になります。
# 依存性注入とは?
# 依存性注入(Dependency Injection、DI)は、オブジェクト指向プログラミングにおける設計パターンの一つで、クラスや関数が必要とする依存関係(他のクラスやサービスなど)を外部から提供する方法です。
# これにより、コードの柔軟性、再利用性、テスト容易性が向上します。
# yieldって何をしているの?
# yieldはPythonのジェネレーター関数で使用されるキーワードで、関数の実行を一時停止し、値を返すために使われます。
# ここでは、データベースセッションを生成し、そのセッションを呼び出し元に提供した後、finallyブロックでセッションを閉じるために使用されています。
# つまりサーバーが起動している間常にDBと接続しているわけではなく、これのおかげで必要なときに接続して、使い終わったら切断しているということ?
# はい、その通りです。get_db関数は、リクエストごとに新しいデータベースセッションを生成し、使用後に閉じることで、リソースの効率的な管理を実現しています。
# これにより、サーバーが常にデータベースと接続しているわけではなく、必要なときにのみ接続し、使用後に切断することができます。
# 例えばDB参照が不要なリクエストの場合はこれは使われない?
# はい、その通りです。DB参照が不要なリクエストの場合、get_db関数は呼び出されません。
# FastAPIの依存関係注入システムは、エンドポイント関数がデータベースセッションを必要とする場合にのみ、この関数を実行します。これにより、不要なデータベース接続のオーバーヘッドを避けることができます。
def get_db():
"""データベースセッションを取得する依存関係"""
db = SessionLocal()
try:
yield db
finally:
db.close()
DI を使うメリット
- DB close 漏れの心配がなくなる
- テストが書きやすくなる
- router がスリムになる
⑦ lifespan(アプリ起動イベント)
FastAPI の on_event("startup") は非推奨となり
lifespan イベントで初期処理を行う必要があります。
例えば:
- DB のテーブルを create_all する
- キャッシュを warming する
- 初回データを seed する
などの処理をアプリ起動時に実行できます。
# ② FastAPI インスタンスを lifespan 付きで作成
app = FastAPI(lifespan=lifespan)
🔍 FastAPI の流れ:実際に API が動くまで
以下のように処理が流れます:
Vue(HTTPリクエスト)
↓
router(URLごとの入口)
↓
Pydantic(入力の型チェック)
↓
CRUD(ビジネスロジック)
↓
SQLAlchemy(DB処理)
↓
DB(MySQL)
↓
レスポンス → Pydantic → router → Vue
📘 FastAPI のメリットまとめ
✔ 型ベースの API 開発が圧倒的に書きやすい
Pydantic により、型が Swagger に自動反映されます。
✔ 高速(Starlette + uvicorn ベース)
Node.js や Flask と比べても圧倒的な速度。
✔ 実務で十分使える柔軟性
- router 分離
- DI
- ミドルウェア
- lifespan
- テストが書きやすい構造
✔ Swagger(OpenAPI)が標準搭載
API Docs の生成が自動なので、フロント連携が楽。
🎉 まとめ:FastAPI の全体像が理解できると開発が加速する
この記事では FastAPI 初心者に向けて:
- ディレクトリ構成
- Model / Schema / Router / CRUD の違い
- DI(依存性注入)
- lifespan
- FastAPI アプリの流れ
- なぜこの構造が実務的なのか
を丁寧に解説しました。
FastAPI を理解するポイントは:
“HTTP の入口(router)と、DB やロジック(CRUD)をきちんと分離する”
これだけでコードが非常に綺麗になり、
保守性の高いプロジェクトが組めるようになります。
次回は、DBマイグレートについても書こうと思います。
→記事