- API
- 技術 💻
FastAPI入門|uvで作るCRUD API完成コードと実行結果
FastAPIとは、Pythonの型ヒントを書くだけで入力チェック・JSON変換・APIドキュメント(Swagger UI)までを自動で用意してくれるWeb APIフレームワークです。 関数に title: str や priority: int と型を書くと、その型に合わないリクエストは自動で422エラーになり、/docs を開けばブラウザからAPIを試せます。
この記事では、2026年9月時点の最新版(FastAPI 0.141.1、Pydantic 2.13.5、SQLModel 0.0.47、Python 3.13)で、タスク管理のCRUD API(作成・一覧・取得・更新・削除)を、uvで環境を作るところから完成まで進めます。コードはすべてコピペで動く形で載せ、curlとpytestで実際に叩いたレスポンス(201/200/204/404/422のJSON)をそのまま掲載しています。Swagger UIをブラウザ自動操作(Playwright)で開いて確認した結果や、async def と def の速度差の実測も紹介します。
FastAPI入門の早見表
| やりたいこと | 書き方(2026年の標準) | この記事の該当箇所 |
|---|---|---|
| プロジェクトを作る | uv init task-api → uv add "fastapi[standard]" |
環境構築 |
| 開発サーバーを起動 | uv run fastapi dev app/main.py(自動リロードつき) |
起動 |
| 本番用に起動 | uv run fastapi run app/main.py |
起動 |
| パスパラメータ | def read(task_id: int) + @app.get("/tasks/{task_id}") |
読み取り |
| クエリパラメータ | limit: Annotated[int, Query(ge=1, le=100)] = 20 |
一覧 |
| リクエストボディ | Pydanticモデル(SQLModel)を引数に書く | 作成 |
| レスポンスの形を決める | response_model=TaskPublic |
モデル |
| ステータスコード | status_code=status.HTTP_201_CREATED |
作成 |
| 404を返す | raise HTTPException(status_code=404, detail=...) |
取得 |
| DBセッションを渡す | SessionDep = Annotated[Session, Depends(get_session)] |
依存性注入 |
| 起動時の処理 | lifespan(@app.on_event は非推奨) |
main.py |
| ファイル分割 | APIRouter(prefix="/tasks") + app.include_router() |
ルーター |
| 部分更新 | model_dump(exclude_unset=True) |
更新 |
| テスト | TestClient + app.dependency_overrides |
テスト |
| API仕様の確認 | /docs(Swagger UI)、/redoc、/openapi.json |
ドキュメント |
| CORS | app.add_middleware(CORSMiddleware, allow_origins=[...]) |
CORS |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「FastAPI入門|uvで作るCRUD API完成コードと実行結果」
FastAPIの特徴と、この記事で使うバージョン
FastAPIは「型ヒント=仕様」になるのが最大の特徴です。 内部ではHTTP処理を Starlette、データの検証を Pydantic が担当し、OpenAPI 3.1 の仕様書(/openapi.json)と Swagger UI(/docs)を自動生成します。
この記事で実際に動かしたバージョンは次のとおりです(uv pip list で確認)。
| パッケージ | バージョン |
|---|---|
| Python | 3.13 |
| fastapi | 0.141.1 |
| fastapi-cli | 0.0.32 |
| pydantic | 2.13.5 |
| sqlmodel | 0.0.47(内部の SQLAlchemy は 2.0.54) |
| starlette | 1.7.0 |
| uvicorn | 0.54.0 |
| pytest | 9.1.1 |
ORMはFastAPI公式チュートリアルに合わせてSQLModelを選びました。 SQLModelのクラスは「DBのテーブル定義」と「Pydanticの入力・出力モデル」を兼ねられるので、入門で覚えることが少なく済みます。より細かいSQL制御が必要になったら、同じ考え方でSQLAlchemy 2.0に移れます。
以前の記事の書き方との違い
2023年以前の解説と比べて、主に次の書き方が変わっています。 古い記事のコードを参考にするときは置き換えてください。
| 以前の書き方 | 2026年の書き方 | 理由 |
|---|---|---|
pip install fastapi uvicorn |
uv add "fastapi[standard]" |
CLI・uvicorn・httpxなどが一括で入る |
uvicorn main:app --reload |
fastapi dev main.py |
FastAPI CLIが標準に |
class Config: orm_mode = True |
model_config = ConfigDict(from_attributes=True) |
Pydantic v2(FastAPI 0.100.0 以降) |
.dict() |
.model_dump() |
Pydantic v2 |
.from_orm(obj) |
.model_validate(obj) |
Pydantic v2 |
limit: int = Query(20, le=100) |
limit: Annotated[int, Query(le=100)] = 20 |
Annotated が推奨 |
@app.on_event("startup") |
lifespan |
on_event は非推奨 |
Optional[str] |
str | None |
Python 3.10 以降の書き方 |
環境構築:uvでプロジェクトを作る
uvはPythonのバージョン管理・仮想環境・パッケージ管理を1つでこなす高速なツールです。 次のコマンドでプロジェクトを作ります。
uv init task-api -p 3.13
cd task-api
rm main.py # uv init が作るサンプルは使わない
uv add "fastapi[standard]" sqlmodel
uv add --dev pytest httpx2
mkdir -p app/routers tests
touch app/__init__.py app/routers/__init__.py tests/__init__.py
fastapi[standard]には、fastapiコマンド(fastapi-cli)、uvicorn、httpx などが含まれます。httpx2はテスト用です。Starlette 1.7.0 の TestClient はhttpxのままでも動きますが、実行すると次の警告が出ました。httpx2を入れると警告は消えます。
StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
完成時のフォルダ構成です。
task-api/
├── pyproject.toml
├── app/
│ ├── __init__.py
│ ├── main.py # アプリ本体(lifespan・CORS・ルーター登録)
│ ├── database.py # DB接続とセッションの依存性
│ ├── models.py # SQLModel のモデル
│ └── routers/
│ ├── __init__.py
│ └── tasks.py # /tasks のエンドポイント
└── tests/
├── __init__.py
└── test_tasks.py
完成コード:タスク管理CRUD API(4ファイル)
database.py:DB接続と依存性(Depends)
# app/database.py
from collections.abc import Iterator
from typing import Annotated
from fastapi import Depends
from sqlmodel import Session, SQLModel, create_engine
DATABASE_URL = "sqlite:///tasks.db"
# SQLite を複数スレッドから使うための設定(FastAPI公式チュートリアルと同じ)
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
def create_db_and_tables() -> None:
SQLModel.metadata.create_all(engine)
def get_session() -> Iterator[Session]:
with Session(engine) as session:
yield session
# 依存性注入:引数に SessionDep と書くだけでセッションが渡される
SessionDep = Annotated[Session, Depends(get_session)]
依存性注入(Depends)とは、「この処理に必要なもの」をFastAPIに用意してもらう仕組みです。 get_session は yield でセッションを渡し、レスポンスを返し終わると with を抜けてセッションが閉じます。Annotated[Session, Depends(get_session)] に名前を付けておくと、各エンドポイントでは session: SessionDep と書くだけで済みます。テストでDBを差し替えられるのもこの仕組みのおかげです。
models.py:入力・出力・テーブルを分ける
# app/models.py
from sqlmodel import Field, SQLModel
class TaskBase(SQLModel):
title: str = Field(min_length=1, max_length=100)
description: str | None = Field(default=None, max_length=1000)
priority: int = Field(default=3, ge=1, le=5)
done: bool = False
class Task(TaskBase, table=True):
"""DBのテーブル(table=True)"""
id: int | None = Field(default=None, primary_key=True)
class TaskCreate(TaskBase):
"""POST で受け取る形"""
class TaskUpdate(SQLModel):
"""PATCH で受け取る形(すべて任意)"""
title: str | None = Field(default=None, min_length=1, max_length=100)
description: str | None = None
priority: int | None = Field(default=None, ge=1, le=5)
done: bool | None = None
class TaskPublic(TaskBase):
"""レスポンスで返す形"""
id: int
モデルを「受け取る形」「返す形」「保存する形」に分けるのがポイントです。
| クラス | 役割 | 分ける理由 |
|---|---|---|
TaskBase |
共通の項目と制約 | 重複を書かない |
Task(table=True) |
DBテーブル | id はDBが採番するので None を許す |
TaskCreate |
POSTの入力 | クライアントに id を送らせない |
TaskUpdate |
PATCHの入力 | 全項目を任意にして部分更新できるようにする |
TaskPublic |
レスポンス | id を必ず含める。内部専用の列を足しても外に出ない |
routers/tasks.py:APIRouterでエンドポイントをまとめる
# app/routers/tasks.py
from typing import Annotated
from fastapi import APIRouter, HTTPException, Query, status
from sqlmodel import select
from app.database import SessionDep
from app.models import Task, TaskCreate, TaskPublic, TaskUpdate
router = APIRouter(prefix="/tasks", tags=["tasks"])
def get_task_or_404(session: SessionDep, task_id: int) -> Task:
task = session.get(Task, task_id)
if task is None:
raise HTTPException(status_code=404, detail=f"Task {task_id} not found")
return task
@router.post("/", response_model=TaskPublic, status_code=status.HTTP_201_CREATED)
def create_task(task: TaskCreate, session: SessionDep):
db_task = Task.model_validate(task)
session.add(db_task)
session.commit()
session.refresh(db_task)
return db_task
@router.get("/", response_model=list[TaskPublic])
def list_tasks(
session: SessionDep,
done: bool | None = None,
offset: Annotated[int, Query(ge=0)] = 0,
limit: Annotated[int, Query(ge=1, le=100)] = 20,
):
stmt = select(Task)
if done is not None:
stmt = stmt.where(Task.done == done)
return session.exec(stmt.offset(offset).limit(limit)).all()
@router.get("/{task_id}", response_model=TaskPublic)
def read_task(task_id: int, session: SessionDep):
return get_task_or_404(session, task_id)
@router.patch("/{task_id}", response_model=TaskPublic)
def update_task(task_id: int, task: TaskUpdate, session: SessionDep):
db_task = get_task_or_404(session, task_id)
db_task.sqlmodel_update(task.model_dump(exclude_unset=True))
session.add(db_task)
session.commit()
session.refresh(db_task)
return db_task
@router.delete("/{task_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_task(task_id: int, session: SessionDep):
db_task = get_task_or_404(session, task_id)
session.delete(db_task)
session.commit()
FastAPIは引数の種類を自動で見分けます。
| 引数の書き方 | FastAPIの解釈 |
|---|---|
パスの {task_id} と同名の task_id: int |
パスパラメータ |
int・bool・str など単純な型 |
クエリパラメータ(?done=false&limit=10) |
| Pydantic/SQLModel のモデル | JSONのリクエストボディ |
Annotated[..., Depends(...)] |
依存性(DBセッションなど) |
部分更新の model_dump(exclude_unset=True) は、クライアントが送ってきた項目だけを辞書にします。{"done": true} だけ送れば、title などは元の値のまま残ります。
main.py:lifespan・CORS・ルーター登録
# app/main.py
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.database import create_db_and_tables
from app.routers import tasks
@asynccontextmanager
async def lifespan(app: FastAPI):
create_db_and_tables() # 起動時に1回だけ実行
yield
# ここに終了時の処理(接続のクローズなど)を書ける
app = FastAPI(title="Task API", version="1.0.0", lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"], # フロントエンドのオリジン
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(tasks.router)
@app.get("/health")
def health():
return {"status": "ok"}
lifespanは「yieldの前が起動時、後が終了時」の処理です。 公式ドキュメントでは @app.on_event("startup") は非推奨になっており、lifespanが推奨されています。
起動:fastapi dev と fastapi run
uv run fastapi dev app/main.py --port 7170
実際の起動ログです。
⚡️ Starting FastAPI in development mode
🐍 Using import string: app.main:app
🌐 Server started at http://127.0.0.1:7170
Documentation at http://127.0.0.1:7170/docs
Logs:
INFO: Will watch for changes in these directories: ['.../task-api']
INFO: Uvicorn running on http://127.0.0.1:7170 (Press CTRL+C to quit)
INFO: Started reloader process [16114] using WatchFiles
INFO: Started server process [16116]
INFO: Waiting for application startup.
INFO: Application startup complete.
| コマンド | 用途 | 待ち受け | 自動リロード |
|---|---|---|---|
fastapi dev |
開発 | 127.0.0.1(自分のPCだけ) |
あり |
fastapi run |
本番・コンテナ | 0.0.0.0(外部から接続可) |
なし |
pyproject.toml にエントリポイントを書くと、ファイル名を省略できます。 fastapi-cli 0.0.32 で確認しました。
[tool.fastapi]
entrypoint = "app.main:app"
uv run fastapi dev --port 7172
# 🐍 Using import string: app.main:app と表示され、/health が {"status":"ok"} を返した
curlで叩いた実際のレスポンス
ここからは起動したサーバーに curl でリクエストした実際の結果です。
作成(POST)→ 201 Created
curl -i -X POST http://127.0.0.1:7170/tasks/ \
-H 'Content-Type: application/json' \
-d '{"title":"備品の発注","priority":2}'
HTTP/1.1 201 Created
server: uvicorn
content-length: 79
content-type: application/json
vary: Origin
{"title":"備品の発注","description":null,"priority":2,"done":false,"id":1}
description と done は送っていませんが、モデルの既定値が入っています。
一覧・絞り込み(GET)→ 200 OK
curl http://127.0.0.1:7170/tasks/
curl "http://127.0.0.1:7170/tasks/?done=false&limit=1"
[{"title":"備品の発注","description":null,"priority":2,"done":false,"id":1},{"title":"会議室の予約","description":"金曜 15時","priority":3,"done":false,"id":2}]
[{"title":"備品の発注","description":null,"priority":2,"done":false,"id":1}]
部分更新(PATCH)→ 200 OK
curl -X PATCH http://127.0.0.1:7170/tasks/1 \
-H 'Content-Type: application/json' -d '{"done":true}'
{"title":"備品の発注","description":null,"priority":2,"done":true,"id":1}
削除(DELETE)→ 204、その後の取得 → 404
curl -i -X DELETE http://127.0.0.1:7170/tasks/2
curl -i http://127.0.0.1:7170/tasks/2
HTTP/1.1 204 No Content
HTTP/1.1 404 Not Found
content-type: application/json
{"detail":"Task 2 not found"}
HTTPException の detail が、そのまま {"detail": ...} の形で返ります。
422エラーの読み方(実際のJSON)
422 Unprocessable Content は「リクエストの形が型や制約に合わない」ことを表します。 FastAPIが自動で返すので、エンドポイントの中にチェック処理を書く必要はありません。
制約違反(空文字・範囲外)
curl -X POST http://127.0.0.1:7170/tasks/ \
-H 'Content-Type: application/json' -d '{"title":"","priority":9}'
{"detail":[
{"type":"string_too_short","loc":["body","title"],"msg":"String should have at least 1 character","input":"","ctx":{"min_length":1}},
{"type":"less_than_equal","loc":["body","priority"],"msg":"Input should be less than or equal to 5","input":9,"ctx":{"le":5}}
]}
(見やすいよう改行を入れています。中身は実際のレスポンスのままです)
まず loc を見ると、どこが問題かがわかります。
| キー | 意味 | 例 |
|---|---|---|
loc |
問題の場所。先頭が body/path/query/header |
["body","title"] |
type |
エラーの種類(Pydanticのエラーコード) | string_too_short、missing |
msg |
人が読むための説明 | Field required |
input |
実際に送られた値 | ""、9 |
ctx |
制約の値 | {"min_length":1} |
よく出る422の実例と直し方
実際に送って確認したものです。
| 送ったもの | 返ってきた type と loc |
直し方 |
|---|---|---|
GET /tasks/abc |
int_parsing、["path","task_id"] |
パスに数値を入れる |
{"priority":"high"} |
missing ["body","title"] と int_parsing ["body","priority"] |
必須項目を送る、型を合わせる |
{"title":"x",}(末尾カンマ) |
json_invalid、["body",12]、ctx.error: Illegal trailing comma before end of object |
JSONの文法を直す(12は文字位置) |
Content-Type なしで -d '{"title":"x"}' |
model_attributes_type、["body"] |
-H 'Content-Type: application/json' を付ける |
最後の例は見落としやすいポイントです。 curl の -d は既定で application/x-www-form-urlencoded を送るため、FastAPIはボディをJSONとして読みません(FastAPI 0.112.0 以降の厳密なContent-Typeチェック)。実際のレスポンスは次のとおりでした。
{"detail":[{"type":"model_attributes_type","loc":["body"],"msg":"Input should be a valid dictionary or object to extract fields from","input":"{\"title\":\"x\"}"}]}
末尾スラッシュで307になる
@router.get("/") と prefix="/tasks" の組み合わせでは、正しいURLは /tasks/ です。/tasks にアクセスすると次のようにリダイレクトされました。
HTTP/1.1 307 Temporary Redirect
ブラウザは自動で /tasks/ へ移動しますが、curl(-L なし)や httpx(既定で follow_redirects=False)は307を受け取ったところで止まります。URLは定義どおり /tasks/ で呼ぶのが確実です。
Swagger UI(/docs)で確認する
http://127.0.0.1:7170/docs を開くと、コードから自動生成されたAPIドキュメントが表示されます。 Playwright 1.63.0(Chromium)で実際に開き、画面の内容を取得した結果です。
title: Task API - Swagger UI
POST /tasks/ Create Task
GET /tasks/ List Tasks
GET /tasks/{task_id} Read Task
PATCH /tasks/{task_id} Update Task
DELETE /tasks/{task_id} Delete Task
GET /health Health
tags: [ 'tasks', 'default' ]
APIRouter(tags=["tasks"])を付けた5つは「tasks」グループに、/healthは「default」グループに並びました。- 説明文(Create Task など)は関数名
create_taskから自動で作られています。 - POSTの「Try it out」で
{"title":"Swaggerから登録","priority":1}を送信すると、画面上に status: 201 とレスポンスJSONが表示されました。
/openapi.json も "openapi": "3.1.0" で上の6つのパスを含んでいました。
補足:Swagger UI のJS・CSSは既定で cdn.jsdelivr.net から読み込まれます。検証環境は外部CDNにつながらないため、Playwright側で同じファイル(npmの swagger-ui-dist 5.33.0)を返して表示しました。社内ネットワークなどでCDNに届かない場合は、公式ドキュメントの「Custom Docs UI Static Assets」の手順で静的ファイルを自前で配信できます。
pytestとTestClientでテストする
app.dependency_overrides でDBセッションの依存性を差し替えると、テストごとにメモリ上の新しいDBを使えます。
# tests/test_tasks.py
import pytest
from fastapi.testclient import TestClient
from sqlmodel import Session, SQLModel, create_engine
from sqlmodel.pool import StaticPool
from app.database import get_session
from app.main import app
@pytest.fixture
def client():
# テストごとにメモリ上の新しいDBを使う
engine = create_engine(
"sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool
)
SQLModel.metadata.create_all(engine)
def get_session_override():
with Session(engine) as session:
yield session
app.dependency_overrides[get_session] = get_session_override
with TestClient(app) as c:
yield c
app.dependency_overrides.clear()
def test_create_and_read(client):
res = client.post("/tasks/", json={"title": "備品の発注", "priority": 2})
assert res.status_code == 201
body = res.json()
assert body == {"title": "備品の発注", "description": None,
"priority": 2, "done": False, "id": 1}
res = client.get(f"/tasks/{body['id']}")
assert res.status_code == 200
assert res.json()["title"] == "備品の発注"
def test_update_and_delete(client):
task_id = client.post("/tasks/", json={"title": "在庫の確認"}).json()["id"]
res = client.patch(f"/tasks/{task_id}", json={"done": True})
assert res.status_code == 200
assert res.json()["done"] is True
assert client.delete(f"/tasks/{task_id}").status_code == 204
assert client.get(f"/tasks/{task_id}").status_code == 404
def test_validation_error(client):
res = client.post("/tasks/", json={"title": "", "priority": 9})
assert res.status_code == 422
locs = [e["loc"] for e in res.json()["detail"]]
assert ["body", "title"] in locs
assert ["body", "priority"] in locs
uv run pytest -v
collecting ... collected 3 items
tests/test_tasks.py::test_create_and_read PASSED [ 33%]
tests/test_tasks.py::test_update_and_delete PASSED [ 66%]
tests/test_tasks.py::test_validation_error PASSED [100%]
============================== 3 passed in 0.57s ===============================
| テストの工夫 | 理由 |
|---|---|
"sqlite://"(ファイル名なし) |
メモリ上のDB。テスト後に何も残らない |
StaticPool |
メモリDBを同じ接続で使い回し、テーブルが見えなくなるのを防ぐ |
with TestClient(app) |
with で使うと lifespan(起動時処理)も実行される |
async def と def の使い分け(実測)
FastAPIでは、def で書いた関数はスレッドプールで実行され、async def の関数はイベントループ上で直接実行されます。 そのため、async def の中で time.sleep() のような「awaitできない待ち」を書くと、その間ほかのリクエストが進みません。
次の3つのエンドポイントに、それぞれ5件を同時に送って時間を測りました(fastapi run、ワーカー1つ)。
# blocking_demo.py
import asyncio
import time
from fastapi import FastAPI
app = FastAPI()
@app.get("/async-blocking")
async def async_blocking():
time.sleep(1) # 同期の待ち(イベントループを止める)
return {"ok": True}
@app.get("/sync")
def sync_endpoint():
time.sleep(1) # def ならスレッドプールで実行される
return {"ok": True}
@app.get("/async-await")
async def async_await():
await asyncio.sleep(1) # await できる待ち
return {"ok": True}
/async-blocking 5件同時: 5.03秒
/sync 5件同時: 1.02秒
/async-await 5件同時: 1.01秒
async def の中で同期処理をすると、5件が1件ずつ順番に処理されて5秒かかりました。 判断の目安は次のとおりです。
| 中で使う処理 | 書き方 |
|---|---|
await できるライブラリ(httpx.AsyncClient、asyncpg など) |
async def |
| 同期ライブラリ(requests、今回の SQLModel の Session など) | def |
| 迷ったとき | def(FastAPIがスレッドプールで動かしてくれる) |
この記事のCRUD APIは同期の Session を使うため、すべて def で書いています。
CORSの設定(実際のヘッダー)
CORSとは、ブラウザが「別のオリジン(ドメイン・ポート)のAPIを呼んでよいか」を確かめる仕組みです。 フロントエンドを http://localhost:5173 で動かし、APIを http://127.0.0.1:7170 で動かすと、オリジンが違うのでCORSの設定が必要になります。
許可したオリジンからのプリフライト(事前確認の OPTIONS リクエスト)の結果です。
curl -i -X OPTIONS http://127.0.0.1:7170/tasks/ \
-H 'Origin: http://localhost:5173' \
-H 'Access-Control-Request-Method: POST' \
-H 'Access-Control-Request-Headers: content-type'
HTTP/1.1 200 OK
access-control-allow-methods: DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, QUERY
access-control-max-age: 600
access-control-allow-origin: http://localhost:5173
access-control-allow-headers: content-type
許可していない http://localhost:3000 から送ると、次のようになりました。
HTTP/1.1 400 Bad Request
Disallowed CORS origin
| 設定 | 使いどころ |
|---|---|
allow_origins=["http://localhost:5173"] |
許可するオリジンを列挙する(基本はこれ) |
allow_origins=["*"] |
認証なしの公開APIなど |
allow_credentials=True |
Cookieを送る場合。許可するオリジンは具体的に列挙するのが安全 |
curl やサーバー間通信にはCORSは関係なく、ブラウザからの呼び出しだけに効きます。「curlでは動くのにブラウザからだと失敗する」ときは、まずCORSを確認するとよいです。
よくあるつまずきチェックリスト
| 症状 | 原因 | 対処 |
|---|---|---|
422 model_attributes_type |
Content-Type が JSON でない | -H 'Content-Type: application/json' |
422 missing |
必須項目がない、またはキー名の違い | loc の最後の要素のキー名を確認 |
| 307 が返る | 末尾スラッシュの有無 | 定義どおりのURLで呼ぶ |
orm_mode のエラー・警告 |
Pydantic v1の書き方 | model_config = ConfigDict(from_attributes=True) |
| on_event の非推奨警告 | 古い起動処理 | lifespan に置き換え |
| 同時アクセスで遅い | async def 内で同期処理 |
def にするか、await できるライブラリに替える |
| ブラウザからだけ失敗 | CORS未設定 | CORSMiddleware でオリジンを許可 |
よくある質問
Q. SQLModelとSQLAlchemyはどちらを使えばよいですか?
A. 入門なら、FastAPI公式チュートリアルが使っている SQLModel が学びやすいです。モデルを1つ書けばDBテーブルと入力チェックの両方に使えます。複雑な結合や細かいSQL制御が増えてきたら SQLAlchemy 2.0 を直接使う形に広げられます。SQLModel は内部で SQLAlchemy を使っているので、select() などの考え方はそのまま通用します。
Q. 本番ではSQLiteのままでよいですか?
A. 同時書き込みが少ない小規模な用途ならSQLiteでも動きます。複数サーバーで動かす場合や書き込みが多い場合は、PostgreSQL などに切り替えます。DATABASE_URL を postgresql+psycopg://... に変え、check_same_thread の設定を外せば、モデルやエンドポイントのコードはそのまま使えます。
Q. 422のメッセージを日本語にできますか?
A. RequestValidationError の例外ハンドラーを @app.exception_handler(RequestValidationError) で登録すると、レスポンスの形を自由に組み立てられます。type(string_too_short など)をキーにして日本語メッセージの対応表を作る方法が扱いやすいです。
まとめ
- FastAPIは型ヒントから入力チェック・JSON変換・Swagger UIまで自動で用意するWeb APIフレームワークです。
- 2026年の構成は
uv add "fastapi[standard]"→fastapi dev。Pydantic v2、Annotated、lifespan が標準の書き方です。 - モデルは Create/Update/Public/テーブルに分けると、入力と出力を安全にコントロールできます。
- DBセッションは
Dependsで注入し、テストではdependency_overridesでメモリDBに差し替えます。 - 422は
locを見れば原因の場所がわかります。Content-Type の付け忘れにも気をつけます。 async defの中で同期処理をすると同時処理が止まるため、迷ったらdefで書きます(実測で5.03秒と1.02秒の差)。- ブラウザから呼ぶなら CORSMiddleware で許可するオリジンを列挙します。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「FastAPI入門|uvで作るCRUD API完成コードと実行結果」
株式会社RJCでは、FastAPIのようなモダンなフレームワークを使ったAPI開発の知見を、プロジェクトの中で仲間と共有しています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。