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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?