概要
本記事では、Python の高速 Web フレームワーク FastAPI の「動作原理」を、実際にコードを動かしながら段階的に解説します。
単なるチュートリアルではなく、FastAPI がどのように動作しているのかを理解することを目的としています。
シンプルな構成のアプリケーションを題材に、リクエストの流れ・非同期処理・依存関係注入の仕組みなどを丁寧に追っていきます。
下表の技術選定を前提としています。
| 項目 | 使用技術 |
|---|---|
| フレームワーク | FastAPI |
| パッケージマネージャー | uv |
| データベース | PostgreSQL |
| ORM | SQLModel |
| ドライバー | asyncpg |
選定理由に関してはこちらの記事の技術選定セクションをご覧ください。
この記事で学べること
-
lifespanによるアプリ起動時・終了時処理の流れ -
Depends()による依存関係注入の実装と実行タイミング - FastAPI アプリケーションがリクエストを処理する仕組み
- SQLModel と SQLAlchemy の関係、および ORM 層の動作
- 非同期処理 (
async・await) が FastAPI 内でどのように動いているか
想定読者
- Python での Web アプリケーション構築に基礎から取り組みたい方
- FastAPI の内部動作を理解したいエンジニア
- SQLModel や asyncpg を用いた非同期 ORM の仕組みを知りたい方
- クリーンアーキテクチャに発展させる前段階として、FastAPI の基本原理を整理したい方
簡易アプリケーション構築
この節では、まず全体像を掴むことを目的に、コードの詳細な解説を行わずに一気にアプリケーションを構築します。
細かな理論や内部構造の説明は後半で詳しく扱います。
1. ディレクトリ構成
まずは uv を使ってライブラリを載せた仮想環境を起動します。
# パッケージマネージャーのインストール
brew install uv
# 環境の初期化
uv init --python 3.14
# 依存関係の追加
uv add fastapi uvicorn sqlmodel asyncpg psycopg[binary]
動作原理の解説が目的のため、以下のようなシンプルな構成を採用します。
この構成になるよう、ご自身の環境で設定をお願いいたします。
myproject/
├── app/
│ ├── main.py
│ ├── db.py
│ └── models.py
└── pyproject.toml
2. SQLModel で DB を定義
app/models.py に、PostgreSQL と接続するためのモデルを定義します。
from sqlmodel import SQLModel, Field
class User(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str
email: str
3. 非同期 DB エンジン設定
app/db.py に asyncpg を使って非同期エンジンを構築し、アプリ起動時にテーブルを自動作成します。
from sqlmodel import SQLModel
from sqlmodel.ext.asyncio.session import AsyncSession
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
DATABASE_URL = "postgresql+asyncpg://user:password@localhost:5432/sample_db"
engine = create_async_engine(DATABASE_URL, echo=True)
AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False, class_=AsyncSession)
async def init_db() -> None:
async with engine.begin() as conn:
await conn.run_sync(SQLModel.metadata.create_all)
async def get_session() -> AsyncSession:
async with AsyncSessionLocal() as session:
yield session
4. FastAPI アプリ本体
app/main.py で lifespan を使って、アプリ起動時と終了時の処理を非同期で管理します。
from contextlib import asynccontextmanager
from fastapi import FastAPI, Depends
from sqlmodel import select
from app.db import init_db, get_session
from app.models import User
from sqlmodel.ext.asyncio.session import AsyncSession
@asynccontextmanager
async def lifespan(app: FastAPI):
print("Initializing database...")
await init_db()
yield
print("Shutting down...")
app = FastAPI(
title="FastAPI",
lifespan=lifespan,
)
@app.post("/users")
async def create_user(user: User, session: AsyncSession = Depends(get_session)):
session.add(user)
await session.commit()
await session.refresh(user)
return user
動作原理の解説
続いて、ここまでで作成した環境を確認しながら、アプリケーションの動作原理を詳しく見ていきます。
1. アプリ起動時の仕組み
FastAPI アプリケーションを起動するとき、内部では ASGI サーバー(Uvicorn)が動作し、アプリのライフサイクルが順に処理されます。
ここでは、次の 3 つのポイントに分けて解説します。
- Uvicorn による ASGI アプリの起動
-
lifespanコンテキストマネージャーの呼び出しタイミング -
init_db()によるテーブル作成の仕組み
1.1. Uvicorn による ASGI アプリの起動
アプリは次のコマンドで起動します。
uv run uvicorn app.main:app --reload
このコマンドの意味を一つずつ分解すると、次のようになります。
| 部分 | 意味 |
|---|---|
uv run |
uv パッケージマネージャー経由でコマンドを実行する(仮想環境の設定を自動で行う) |
uvicorn |
Python の軽量・高性能な ASGI サーバー(FastAPI はこの上で動作) |
app.main:app |
app/main.py ファイル内の app オブジェクトを指定 |
--reload |
ファイル更新時に自動で再起動(開発モード用) |
ASGI (Asynchronous Server Gateway Interface) とは
「Web サーバー」と「Python アプリケーション」をつなぐための通信ルール(インターフェース仕様)です。
簡単に言えば…
「リクエストをどう受け取り、レスポンスをどう返すか」をサーバーとアプリが共通の約束事として決めたものです。
Web アプリは単独では動けず、実際にブラウザと通信するのはサーバー(Uvicorn など)です。
ASGI は、このサーバーがアプリ(FastAPI など)を呼び出すときの「話し方(プロトコル)」を定義しています。
例えるなら…
- Uvicorn — 電話を受け取るオペレーター(通信担当)
- FastAPI アプリ — 実際に話す担当者(アプリロジック担当)
- ASGI — 両者が会話するときの共通言語(日本語のようなもの)
このおかげで、Uvicorn 以外のサーバー(例:Hypercorn・Daphne)でも FastAPI を動かせます。
つまり、「どの ASGI サーバーでも動く」=サーバー実装に依存しない仕組みです。
ASGI の基本動作イメージ
FastAPI の内部では、次のような流れでリクエストが処理されます。
- ブラウザが http://localhost:8000/users にアクセス
- Uvicorn(ASGI サーバー)が HTTP リクエストを受け取る
- Uvicorn が ASGI 仕様に従い、FastAPI アプリの
__call__()関数を非同期で呼び出す - FastAPI がルーティングを解析して対応する関数(例:
create_user())を実行 - FastAPI がレスポンスを ASGI 仕様で Uvicorn に返却
- Uvicorn がそれを HTTP レスポンスとしてブラウザに返す
まとめ
- ASGI はサーバー(Uvicorn)とアプリ(FastAPI)をつなぐ通信ルール
- リクエストは ASGI 経由でアプリに渡り、レスポンスも ASGI を通して返される
- FastAPI が非同期に動けるのは、ASGI が
async・awaitベースの処理を標準化しているから - Uvicorn はその ASGI プロトコルを理解して、FastAPI を「正しく起動・制御」する役目を担う
1.2. lifespan コンテキストマネージャーの呼び出しタイミング
FastAPI では、アプリ起動時・終了時の処理を @asynccontextmanager デコレータを使って lifespan 関数にまとめます。
from contextlib import asynccontextmanager
from fastapi import FastAPI
from app.db import init_db
@asynccontextmanager
async def lifespan(app: FastAPI):
print("Initializing database...")
await init_db()
yield
print("Shutting down...")
app = FastAPI(
title="FastAPI",
lifespan=lifespan,
)
yield の前が「起動時の処理」、yield の後が「終了時の処理」として扱われます。
非同期コンテキストマネージャーとは
Python には「コンテキストマネージャー」という構文があり、`with` や `async with` を使って、前処理(開始時)と後処理(終了時)をまとめて記述できます。例えば…
async with connect_to_database() as conn:
await conn.query("SELECT * FROM users")
この構文は内部的に次のように動きます。
| タイミング | 実際に呼ばれるメソッド | 役割 |
|---|---|---|
async with に入るとき |
__aenter__() |
前処理(リソース確保・初期化など) |
async with を抜けるとき |
__aexit__() |
後処理(リソース解放・クリーンアップなど) |
つまり…
async with は非同期的に「開始処理 → 本処理 → 終了処理」を安全に実行するための仕組みです。
① Uvicorn が FastAPI アプリを起動
- FastAPI は
lifespan()を呼び出し、__aenter__()相当の部分(yield前)を実行 - このタイミングで
init_db()(テーブル作成など)が走る
② アプリが稼働中
-
yieldによってアプリ本体のリクエスト処理受付が開始される
③ アプリ終了時
- FastAPI が
lifespan()のyield後の処理(__aexit__()相当)を実行 - 接続解除・キャッシュ破棄などの後処理がここで行われる
1.3. init_db() の仕組み — SQLModel によるテーブル作成の流れ
アプリ起動時、lifespan() の中で呼ばれている init_db() 関数は、SQLModel(= SQLAlchemy + Pydantic の統合ライブラリ)を使ってデータベースのテーブルを自動生成しています。
async def init_db() -> None:
async with engine.begin() as conn:
await conn.run_sync(SQLModel.metadata.create_all)
たった数行ですが、裏では以下のような流れで PostgreSQL に実際のテーブルを作成しています。
① engine.begin で非同期 DB 接続を開始
async with engine.begin() as conn:
ここで engine は次のように定義されています。
from sqlalchemy.ext.asyncio import create_async_engine
engine = create_async_engine(DATABASE_URL, echo=True)
create_async_engine() により、非同期で DB 接続を扱うエンジンが作成されます。
async with engine.begin() によって、トランザクションを開始します。
as conn の部分は、実際に SQL を発行するための接続オブジェクト(connection)を受け取ります。
② run_sync で同期処理を非同期環境から実行
await conn.run_sync(SQLModel.metadata.create_all)
run_sync() は少し特殊なメソッドで、「同期関数を非同期環境で実行するためのラッパー」です。
SQLModel.metadata.create_all は同期的な関数であり、そのままでは await できません。
そのため run_sync() を使って内部的にスレッドを切り替え、非同期コード (async def) の中でも安全に実行できるようにしています。
③ SQLModel.metadata.create_all がテーブルを作成
ここが実際の「テーブル作成」を担当している部分です。
SQLModel.metadata.create_all
これは SQLAlchemy の Metadata オブジェクトを通して登録済みのすべてのモデルクラス(table=True のついたクラス)を読み込み、それらに対応する CREATE TABLE 文を自動生成・実行します。
たとえば、以下のモデルを定義していた場合、
from sqlmodel import SQLModel, Field
class User(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str
email: str
SQLModel が内部的に変換して、次のような SQL を実行します。
CREATE TABLE user (
id SERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
email VARCHAR NOT NULL
);
④ echo=True により、SQL ログがターミナルに出力される
create_async_engine() に echo=True を指定しているため、アプリ起動時に実行された SQL がログ出力されます。
INFO sqlalchemy.engine.Engine BEGIN (implicit)
INFO sqlalchemy.engine.Engine CREATE TABLE user ...
INFO sqlalchemy.engine.Engine COMMIT
このログを見ることで、「どんな SQL が自動生成されたか」「DB との通信が正常に行われたか」を確認できます。
1.4. アプリ起動時のまとめ
| 処理 | タイミング | 内容 |
|---|---|---|
uvicorn app.main:app |
アプリ起動時 | Uvicorn が ASGI アプリを読み込み開始 |
lifespan() の開始部 |
サーバー起動前 |
init_db() が呼ばれ、DB を初期化 |
yield |
起動完了 | FastAPI がリクエスト処理を開始 |
yield の後 |
終了時 | 終了処理(クリーンアップなど)が実行される |
このように、FastAPI は単にルーターを起動するだけでなく、アプリ全体のライフサイクルを非同期的に管理しています。
2. 依存関係注入
これ以降の解説を続けるにあたり、FastAPI における 依存関係注入(Dependency Injection) という仕組みの解説をしなければなりません。
これは「関数が必要とするオブジェクト(例:DB セッション)を自動的に用意して渡す」仕組みです。
たとえば次のコードに登場する Depends(get_session) がそれにあたります。
@app.post("/users")
async def create_user(user: User, session: AsyncSession = Depends(get_session)):
session.add(user)
await session.commit()
await session.refresh(user)
return user
このコードでは、create_user 関数が引数 session を要求していますが、関数を呼び出す側(=クライアント)はセッションを渡していません。
代わりに Depends(get_session) と書くことで、FastAPI が自動で用意してくれます。
2.1. Depends の基本的な仕組み
Depends() は FastAPI に対して「この関数を実行する前に、別の関数を呼び出して結果を渡してほしい」と伝えるための宣言です。
上の例では、FastAPI が以下のように動きます。
-
/usersへのリクエストを受け取る -
create_userを実行する前に、get_session()を呼び出す -
get_session()が返したAsyncSessionオブジェクトをsession引数に渡す -
create_user関数を実行する - 関数の処理が終わると、セッションを自動で閉じる
つまり、開発者が明示的にセッションを開閉しなくても、FastAPI が適切なタイミングでライフサイクルを管理してくれます。
2.2. get_session の中身を分解して理解する
async def get_session() -> AsyncSession:
async with AsyncSessionLocal() as session:
yield session
get_session は非同期ジェネレーター関数です。
yield の前後で「リクエストの前処理」と「後処理」を定義できるのがポイントです。
-
async with AsyncSessionLocal() as session:— 新しいデータベースセッションを生成 -
yield session— FastAPI がsessionを依存関係として関数に渡す - その後、リクエスト処理が終わると、
async withブロックを抜けてセッションを自動的に閉じる
これにより、各リクエストごとに新しいセッションが確実に作られ、使い終わったら閉じられるという動作が保証されます。
2.3. なぜ「リクエストごと」に新しいセッションが作られるのか
SQLModel(SQLAlchemy)で扱う AsyncSession はスレッドセーフではありません。
そのため、複数のリクエストで同じセッションを共有すると、データ不整合やトランザクション競合の原因になります。
そこで FastAPI は、get_session() のような依存関数を毎回呼び出し、各リクエスト専用のセッションを生成します。
つまり、次のような流れになります。
① ユーザー A が /users に POST → 新しい AsyncSession が生成される
② ユーザー B が /users に POST → もう 1 つ別の AsyncSession が生成される
③ それぞれの処理が終わると、自動で閉じられる
これにより、リクエスト間でセッションが干渉することはありません。
2.4. 非同期ジェネレーター依存関数のライフサイクルまとめ
| タイミング | 動作内容 |
|---|---|
| リクエスト開始前 | 関数が呼び出され、yield まで実行される |
| リクエスト処理中 |
yield の戻り値が依存先に渡される |
| リクエスト完了後 |
yield の後の処理(例:クリーンアップ)が実行される |
これにより、DB 接続や外部リソースのクリーンな管理が可能になります。
3. リクエスト処理フロー(POST /users の内部動作)
ここでは、実際に /users エンドポイントにリクエストが送信されたとき、FastAPI がどのように内部で処理を進めているのかを順を追って解説します。
アプリ起動後、以下のような POST リクエストを送るとします。
curl -X POST \
-H "Content-Type: application/json" \
-d '{"name": "Alice", "email": "alice@example.com"}' \
http://localhost:8000/users
これにより FastAPI は次のような流れで処理を実行します。
3.1. リクエスト受信とルーティング
まず、Uvicorn(ASGI サーバー)が HTTP リクエストを受け取り、ASGI 仕様に従って FastAPI アプリに制御を渡します。
FastAPI はルーティング定義を確認し、@app.post("/users") に対応する関数を特定します。
@app.post("/users")
async def create_user(user: User, session: AsyncSession = Depends(get_session)):
session.add(user)
await session.commit()
await session.refresh(user)
return user
この時点で、FastAPI はエンドポイント関数に渡す 引数の依存関係 を解決し始めます。
3.2. Pydantic モデルによるリクエストバリデーション
リクエストボディの JSON は、自動的に User モデル(SQLModel 継承)にマッピングされます。
class User(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str
email: str
FastAPI は内部で Pydantic を利用して、型・必須項目・形式の整合性を検証します。
たとえば email が欠けていたり、文字列でない場合は、リクエスト処理が関数に到達する前に 422 Unprocessable Entity を返します。
3.3. 依存関数の解決と DB セッションの取得
次に、Depends(get_session) が評価されます。
async def get_session() -> AsyncSession:
async with AsyncSessionLocal() as session:
yield session
FastAPI はこの関数を呼び出し、yield で返された AsyncSession インスタンスを create_user() 関数の session 引数として注入します。
この仕組みにより、各リクエストごとに独立したデータベースセッションが生成され、同時アクセスでも安全に並列処理できます。
3.4. SQLModel → SQLAlchemy 経由で INSERT 実行
次に、ユーザー定義関数内の以下のコードが実行されます。
session.add(user)
await session.commit()
await session.refresh(user)
-
session.add(user)— SQLAlchemy のセッションにオブジェクトを登録(まだ DB 反映はされない) -
await session.commit()— トランザクションを確定し、INSERT 文を実行 -
await session.refresh(user)— DB から再取得して、idなど自動採番カラムを更新
これにより、DB 上に新しいユーザーが登録され、Python オブジェクトにも反映されます。
3.5. 非同期処理の流れとレスポンス返却
これらの処理はすべて 非同期関数 (async def) 内で実行されています。
FastAPI は内部で イベントループ(asyncio) を管理し、await キーワードの部分でブロッキングを回避しつつ、I/O 処理を効率的に切り替えます。
ここに関してはやや抽象的なため、後の節で解説します。
すべての処理が完了すると、return user により Pydantic モデルが再シリアライズされ、JSON 形式でクライアントに返されます。
{
"id": 1,
"name": "Alice",
"email": "alice@example.com"
}
3.6. まとめ
/users エンドポイントの 1 リクエスト処理には、FastAPI・Pydantic・SQLModel・SQLAlchemy・asyncio の複数コンポーネントが関与しています。
これらが連携することで、
- 型安全な入力検証(Pydantic)
- 自動セッション管理(Depends)
- 非同期 ORM 操作(SQLModel + SQLAlchemy)
- 高速レスポンス生成(asyncio イベントループ)
という流れが成り立っているといえます。
4. SQLModel と SQLAlchemy の関係
前節では /users エンドポイントのリクエスト処理フローを解説しました。
ここでは、その中心にある SQLModel がどのように SQLAlchemy と連携して動作しているのかを掘り下げて理解していきます。
一見すると、SQLModel だけで ORM(Object Relational Mapper)処理をすべて行っているように見えますが、実際のところその多くは SQLAlchemy の機能をラップ したものです。
つまり、SQLModel は「FastAPI と親和性の高い SQLAlchemy の便利なラッパー」と考えると理解しやすいです。
4.1. SQLAlchemy の上に構築された SQLModel
SQLModel は内部的に SQLAlchemy の DeclarativeBase を継承しており、モデルクラスを定義すると自動的に テーブル定義(メタデータ) が生成されます。
from sqlmodel import SQLModel, Field
class User(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str
email: str
このコードを定義した時点で、SQLAlchemy の Table オブジェクトが内部的に構築され、SQLModel.metadata に登録されます。これが init_db() の中で SQLModel.metadata.create_all() によってテーブルを作成する仕組みです。
4.2. table=True の意味
SQLModel はデータモデルとテーブル定義の両方を担うことができます。
-
table=True— データベーステーブルと対応する 永続化モデル として扱う -
table=False(または省略)— テーブルを持たない Pydantic モデル的用途(スキーマ定義)として扱う
これにより、1 つのモデルを API リクエスト用と DB 永続化用で共通化できます。
class UserBase(SQLModel):
name: str
email: str
class User(UserBase, table=True):
id: int | None = Field(default=None, primary_key=True)
上記のように分けることで、API 入出力(UserBase)と DB 構造(User)を柔軟に分離できます。
4.3. Field() の内部動作
Field() は Pydantic の Field と似ていますが、SQLModel ではそれに加えて SQLAlchemy の Column オブジェクト を生成しています。
つまり、
id: int | None = Field(default=None, primary_key=True)
は、内部的には以下のような SQLAlchemy の設定に変換されます。
id = Column(Integer, primary_key=True, autoincrement=True)
そのため、Field で指定した属性(primary_key・index・nullable・foreign_key など)は、SQLAlchemy のカラム設定に反映されます。
4.4. SQLModel と SQLAlchemy の協調動作
アプリ起動時に SQLModel.metadata.create_all(engine) を呼ぶと、登録済みのすべてのテーブル定義(SQLAlchemy の Table オブジェクト)が実際の DB に反映されます。
クエリ実行時には、SQLAlchemy の Session・AsyncSession が ORM として機能し、session.add() や session.commit() を通じて SQL が発行されます。
SQLModel はその上で、
- 型アノテーションに基づく安全なクエリ
- Pydantic との自動シリアライズ
- FastAPI との自然な統合
を実現しています。
4.5. まとめ
| レイヤ | 主な役割 |
|---|---|
| SQLAlchemy | ORM の中核。テーブル定義・クエリ発行・セッション管理を担う |
| SQLModel | SQLAlchemy をラップし、Pydantic 互換の型安全なモデルを提供する |
SQLModel は単なるラッパーではなく、「SQLAlchemy の強力さ」と「Pydantic の型安全性」を橋渡しする存在です。
これにより、FastAPI アプリは少ないコードで強力な DB 操作とデータ検証を両立させています。
5. 非同期処理の流れ
ここでは、FastAPI アプリケーション内での 非同期処理の仕組み を理解していきます。
前節までで見たように、アプリの主要な関数はすべて async def で定義され、await を使ってデータベース操作などの I/O 処理を行っていました。
これにより、高速かつスケーラブルな同時リクエスト処理が可能になります。
5.1. async def とイベントループ
Python の非同期処理は、標準的に組み込まれているasyncio モジュールによって管理されています。FastAPI もこの仕組みを利用しています。
非同期関数(async def)を呼び出すと、すぐに実行されるのではなく、「コルーチンオブジェクト」 が返されます。
このオブジェクトはイベントループによってスケジュールされ、必要なタイミングで実行されます。
import asyncio
async def say_hello():
print("Hello")
await asyncio.sleep(1)
print("World")
asyncio.run(say_hello())
上記のように await asyncio.sleep(1) に到達すると、処理はいったん中断され、他のタスク(たとえば別のリクエスト処理)が割り込むことができます。
これが ノンブロッキング I/O の基本原理です。
5.2. FastAPI における非同期リクエスト処理
FastAPI は内部で ASGI を介して非同期処理を実現しています。
リクエストが到達すると:
- Uvicorn が HTTP リクエストを受け取る
- FastAPI がエンドポイント関数をコルーチンとして実行
-
awaitがある箇所で処理が一時停止し、他のリクエスト処理に切り替え - 戻ってきたら続きから再開
これにより、1 スレッドで多数のリクエストを効率的に処理できます。
5.3. await session.commit() の内部動作
await session.commit() は SQLAlchemy の非同期エンジン(AsyncEngine)を通じて、実際の SQL コマンドをデータベースへ送信します。
await session.commit()
この呼び出しの裏側では:
- SQLAlchemy がトランザクションを確定するための SQL (
COMMIT) を発行 -
asyncpgドライバーが非同期に PostgreSQL へ送信 - イベントループが I/O 完了を待機
- 結果が返ると処理を再開
つまり、await によって「I/O 待ちの間は CPU をブロックしない」構造になっています。
5.4. 非同期 ORM がスレッドブロッキングを回避する仕組み
従来の同期 ORM では、1 件のクエリ実行中に他のリクエストが待たされてしまう(スレッドブロック)が発生していました。
しかし asyncpg + AsyncSession の組み合わせでは、I/O 待ちの間に別のコルーチンへ制御が移るため、1 スレッドで多リクエスト同時処理 が可能です。
FastAPI はこの仕組みを自然に利用できるよう設計されており、特別なスレッドプールやロック制御を意識せずに高パフォーマンスを実現できます。
5.5. 非同期処理の可視化イメージ
以下は、await によって処理が切り替わるイメージ図です。
┌──────────────┬──────────────┬──────────────┐
│ Task A │ Task B │ Task C │
├──────────────┼──────────────┼──────────────┤
│ DB アクセス待機 CPU 計算 DB 書き込み待機
│ → 一時停止 → 実行中 → 一時停止
└──────────────┴──────────────┴──────────────┘
イベントループはこれらのタスクを切り替えながら実行し、ブロッキングが発生しないようにスケジューリングしています。
5.6. まとめ
| 要素 | 役割 |
|---|---|
| async def・await | 非同期処理の基本構文。I/O 待機中に他タスクへ切り替え |
| asyncio イベントループ | タスクをスケジュールし、効率的に実行する心臓部 |
| AsyncSession・asyncpg | 非同期 DB アクセスを支える ORM とドライバー |
この仕組みにより、FastAPI はシンプルなコードでありながら、数百・数千のリクエストを同時処理できるパフォーマンスを両立させています。
6. 終了処理とリソース解放
ここまでで、FastAPI がアプリを起動し、リクエストを受け取り、DB にアクセスしてレスポンスを返す流れを見てきました。
この節では、アプリが終了するときに何が起きているのか を解説します。
普段あまり意識しませんが、サーバーを停止したときにも FastAPI の内部では「後片付け」のような処理が行われています。
これを理解することで、接続が閉じられないまま残るトラブル(接続リーク) を防ぐことができます。
6.1. lifespan の終了タイミング
前述の通り、lifespan はアプリの 起動から終了までを管理する特別な仕組み です。
yield の前が「起動時の処理」、yield の後が「終了時の処理」として動きます。
終了時の処理の例を見てみましょう。
from contextlib import asynccontextmanager
from fastapi import FastAPI
from app.db import init_db, shutdown_db
@asynccontextmanager
async def lifespan(app: FastAPI):
print("Initializing database...")
await init_db()
yield
print("Shutting down...")
await shutdown_db()
app = FastAPI(lifespan=lifespan)
上のコードでは、アプリを起動するときに init_db() が呼ばれ、終了するときに shutdown_db() が実行されます。
6.2. どのように呼ばれているのか
uvicorn は FastAPI アプリを起動する際、まず ASGI アプリケーションを読み込みます。
このとき FastAPI が lifespan を検出し、次の順番で呼び出します。
- アプリ起動 →
lifespanの「前処理(yield の前)」を実行 - アプリ稼働中 →
yieldの位置で一時停止し、通常のリクエスト処理を受け付ける - アプリ終了時 →
yieldの後ろの処理を実行
この仕組みにより、FastAPI は「起動時に初期化」「終了時にクリーンアップ」を安全に行えます。
6.3. DB 接続の終了処理
アプリが動作している間、SQLAlchemy(SQLModel の内部で利用)は 接続プール を保持しています。
接続プールとは、DB との通信を効率化するために再利用可能な接続をまとめて管理する仕組みです。
ただし、アプリを終了してもこの接続が閉じられないままだと、PostgreSQL の側で「接続が生きたまま残る」問題が起きることがあります。
そのため、終了時に明示的にプールを閉じる処理を入れておくと安全です。
# app/db.py
from sqlalchemy.ext.asyncio import create_async_engine
engine = create_async_engine(DATABASE_URL, echo=True)
async def shutdown_db():
await engine.dispose() # すべての接続を閉じてリソースを解放
これを lifespan の終了処理に追加します。
@asynccontextmanager
async def lifespan(app: FastAPI):
await init_db()
yield
await shutdown_db()
これで、アプリが停止するときに自動的に DB 接続がすべて閉じられます。
6.4. 依存関係で使っているセッションも自動的に解放される
次のような依存関数を使っている場合:
async def get_session() -> AsyncSession:
async with AsyncSessionLocal() as session:
yield session
async with 構文によって、リクエストが終わったタイミングで自動的にセッションが閉じられます。
これにより、各リクエストごとに安全に新しいセッションを使い、完了後に確実に接続を返すことができます。
6.5. 終了処理が重要な理由
アプリを止めたあとも接続が残ってしまうと、次のような問題が起こります。
- PostgreSQL の接続数が上限に達し、新しい接続が作れなくなる
- テスト実行後に接続が残って、次のテストがエラーになる
- 長時間稼働後にリソースを無駄に消費してしまう
終了時に接続を明示的に閉じることは、こうしたトラブルを防ぐ「お片付け」にあたります。
6.6. まとめ
-
lifespanのyieldの前は 起動時の初期化、後は 終了時のクリーンアップ - DB 接続プールを
await engine.dispose()で確実に閉じる -
async withを使った依存関数は、リクエスト終了時にセッションを自動で解放 - 終了処理を正しく書くことで、アプリは安定して安全に動作し続ける
本記事のまとめと次のステップ
ここまで、FastAPI アプリがどのように動作しているかを「起動 → リクエスト処理 → 終了」という流れに沿って見てきました。
最後に、全体像を整理しながら、次に進むための視点をまとめます。
1. FastAPI アプリのライフサイクル全体像
FastAPI のアプリは、ASGI サーバー(たとえば Uvicorn)の管理下で動作します。
起動から終了までの主な流れは次のとおりです。
この図から分かるように、FastAPI の内部では起動・稼働・終了のそれぞれに明確な役割があり、非同期で安全に処理が行われています。
2. 各ステップのポイント整理
| ステップ | 主な処理 | 関連機能 |
|---|---|---|
| アプリ起動 | DB 初期化・環境変数の読み込み |
lifespan の前処理 |
| リクエスト受付 | ルーティング・バリデーション |
FastAPI の内部ルーターと Pydantic
|
| DB 操作 | セッション管理・クエリ発行 |
SQLModel・SQLAlchemy
|
| レスポンス返却 | モデル変換・HTTP 応答 | ResponseModel |
| 終了処理 | 接続プール解放・後片付け |
lifespan の後処理 |
3. FastAPI の設計思想を理解する
FastAPI は「速く」「安全で」「明確に構造化された」API を構築するためのフレームワークです。
内部的には以下のような原則に支えられています。
- 宣言的な設計 — 関数の引数や戻り値の型から、自動的にルーティング・スキーマ・ドキュメントを生成
-
非同期 I/O — Python の
async・awaitにより、スレッドを増やさず高効率に処理 -
明示的なライフサイクル管理 —
lifespanによって起動・終了時の処理を安全に分離 - 型安全な開発体験 — Pydantic による厳密な型チェックと補完サポート
これらを理解しておくと、FastAPI の仕組みをブラックボックスとして扱うのではなく、設計思想として使いこなす ことができます。
4. 次のステップ — アーキテクチャ設計へ
ここまででアプリの動作原理は明確になりました。
次のステップとしては、アプリ全体の構造設計 に進むのが自然です。
FastAPI では、1 ファイルで書き始めても動作しますが、アプリが大きくなると「どこに何を書くべきか」が課題になります。
そのときに役立つのが、アーキテクチャの考え方です。
代表的な構成パターン
| 構成 | 特徴 | FastAPIとの親和性 | 想定規模 |
|---|---|---|---|
| MVC | Web開発の基本構成。導入しやすいが、責務の境界が曖昧になりやすい。 | ★★★☆☆ - ルーターをViewとして扱える。 | 小〜中規模 |
| レイヤードアーキテクチャ | 層ごとに責務を分離。保守性とテスト性が高い。 | ★★★★☆ - FastAPI構成に自然に適用可能。 | 中規模 |
| クリーンアーキテクチャ | 依存方向をドメイン中心に統一。技術要素からビジネスロジックを独立させる。 | ★★★★★ - FastAPIのDI機能と好相性。長期運用に最適。 | 中〜大規模 |
| ヘキサゴナルアーキテクチャ | 外部I/Oをポートとアダプタで抽象化し、ドメインを保護。テストや拡張が容易。 | ★★★★★ - クリーンアーキテクチャに近く、FastAPIで実践しやすい。 | 大規模 |
| マイクロサービスアーキテクチャ | 機能単位で独立APIを構築。スケールしやすく、チーム分割に強い。 | ★★★★☆ - FastAPIの軽量性と高速性がマイクロサービスと好相性。 | 大規模〜超大規模 |
今回学んだ「起動〜終了の流れ」や「依存関係注入」の仕組みは、こうしたアーキテクチャを実現するための土台になります。
5. まとめ
- FastAPI は 起動・稼働・終了 の全段階を非同期で安全に管理する
-
lifespanによって、初期化とクリーンアップの責務を明確にできる - DB セッションや依存関係は、各リクエストごとに自動的に管理される
- 設計思想を理解することで、フレームワークを「正しく使いこなす」ことができる
- 次のステップは、アプリ全体の構造(アーキテクチャ設計)に進むこと