10
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【FastAPI基礎】モデル・スキーマ・CRUD・依存性注入・ルーターを体験してみた

Last updated at Posted at 2025-12-18

🎯 この記事の目的

「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 する

などの処理をアプリ起動時に実行できます。

main.py
# ② 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マイグレートについても書こうと思います。
記事

10
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?