FastAPIプロダクション構成の教科書 — 依存性注入・非同期DB・JWT・テスト・デプロイを一気通貫で
FastAPIプロダクション構成の教科書 — 依存性注入・非同期DB・JWT・テスト・デプロイを一気通貫で
はじめに — 「入門」と「実務」の間にある谷
FastAPIのチュートリアルを一通り終えた方なら、こんな経験があるのではないでしょうか。「Hello Worldは書けた。簡単なCRUDも動いた。でも、実際の本番APIをどう設計すればいいのか、まったく分からない」という壁です。
依存性注入、非同期データベース接続、JWTリフレッシュトークン、Pydantic v2によるバリデーション、テスト戦略、Dockerデプロイ——これらのトピックは個別に解説した記事こそ存在しますが、実務で使える一枚絵として整理された日本語記事はほぼ存在しません。本記事はその空白を埋めることを目的としています。
この記事で作るもの
JWT認証つきREST APIを例に、以下の構成を一気通貫で解説します。
- レイヤードアーキテクチャによるディレクトリ設計
Depends()を使ったサービス層の分離- SQLAlchemy 2.x AsyncSessionによる非同期DB接続
- アクセストークン+リフレッシュトークンのJWT認証
- Pydantic v2
model_validatorによる複合バリデーション - pytest-asyncioによるAPIテスト
- gunicorn + Dockerによるプロダクションデプロイ
対象読者と前提知識
- FastAPIの公式チュートリアルを完了済みの方
- Python 3.12以上、
async/awaitの基礎を理解している方 - 実務経験1〜3年程度の中級者
検証環境(2026年時点)
| ライブラリ | バージョン |
|---|---|
| FastAPI | 0.115〜0.123系 |
| Pydantic | v2.11.x |
| SQLAlchemy | 2.0.30 |
| uvicorn | 0.32 |
| gunicorn | 23.0 |
| Python | 3.12〜3.13 |
インストールは uv add "fastapi[standard]" が2026年現在の推奨手順です。pip install fastapi[all] と異なり、依存関係の解決が高速で、uv.lock によって再現性が保証されます。
リポジトリ構成 — レイヤードアーキテクチャの全体像
推奨ディレクトリ構成
まず完成形のディレクトリ構成を示します。各章でこのツリーのどのファイルを扱うかを意識しながら読み進めてください。
app/
├── api/
│ ├── dependencies/
│ │ ├── auth.py # get_current_user, require_permission
│ │ └── database.py # get_db セッション依存
│ └── routers/
│ ├── auth.py # /auth/* エンドポイント
│ ├── users.py # /users/* エンドポイント
│ └── items.py # /items/* エンドポイント
├── models/ # SQLAlchemy ORM モデル
│ ├── user.py
│ └── item.py
├── schemas/ # Pydantic スキーマ(Request/Response)
│ ├── user.py
│ └── item.py
├── services/ # ビジネスロジック層
│ ├── auth_service.py
│ └── item_service.py
├── crud/ # DB CRUD 操作(生のクエリのみ)
│ ├── user_crud.py
│ └── item_crud.py
├── core/
│ ├── security.py # JWT 処理・パスワードハッシュ
│ └── config.py # pydantic-settings による設定管理
├── database.py # 非同期エンジン・セッションファクトリ
└── main.py
conftest.py
gunicorn.conf.py
Dockerfile
docker-compose.yml
なぜ schemas と models を分けるのか
初学者がよくやってしまうのが、SQLAlchemyのORMモデルをそのままPydanticスキーマとして使い回すパターンです。これは小規模では動きますが、すぐに破綻します。
models/:データベースの永続化構造を表す。テーブル定義の都合で設計するschemas/:APIの外部との契約を表す。クライアントが何を送り、何を受け取るかで設計する
たとえばユーザーモデルには hashed_password カラムが存在しますが、レスポンスには絶対に含めてはいけません。schemas/ で UserResponse を独立させることで、このような情報漏洩を構造レベルで防止できます。
services と crud の責務分離
| 層 | 責務 | 例 |
|---|---|---|
crud/ |
DBへの読み書きのみ | get_user_by_email(db, email) |
services/ |
ビジネスルールの実行 | パスワード検証→トークン生成→ログ記録 |
CRUDはDBを知っていますが、ビジネスルールは知りません。サービス層は複数のCRUD操作とビジネスロジックを組み合わせます。小規模プロジェクトではcrud層を省略してサービス層に直接クエリを書く判断も合理的です。
依存性注入(Depends)によるサービス層設計
Depends() が解決する問題
FastAPIの Depends() は、認証・DBセッション・共通パラメータのDRY化を実現する仕組みです。最大の利点は**「引数に宣言するだけで、テスト時に差し替えられる」**点にあります。
責務ごとに依存を分離する3層設計
# app/api/dependencies/auth.py
from typing import Annotated
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from app.core.security import decode_access_token
from app.models.user import User
from app.crud.user_crud import get_user_by_id
from app.api.dependencies.database import get_db
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")
async def get_current_user(
token: Annotated[str, Depends(oauth2_scheme)],
db: Annotated[AsyncSession, Depends(get_db)],
) -> User:
payload = decode_access_token(token)
if payload is None or payload.get("type") != "access":
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
user = await get_user_by_id(db, int(payload["sub"]))
if user is None:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
return user
def require_permission(required: str):
"""権限チェックを返す高階関数"""
def checker(user: Annotated[User, Depends(get_current_user)]) -> User:
if required not in user.permissions:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
return user
return checkerルーター単位 vs エンドポイント単位の使い分け【中級者が迷うポイント】
# ルーター全体に認証を適用する場合
router = APIRouter(
prefix="/items",
dependencies=[Depends(get_current_user)], # 全エンドポイントに適用
)
# 戻り値が不要な場合(ここが重要)
@router.delete("/{item_id}", dependencies=[Depends(require_permission("admin"))])
async def delete_item(item_id: int, db: Annotated[AsyncSession, Depends(get_db)]):
...
# 戻り値を使う場合(引数として受け取る)
@router.get("/me")
async def get_my_items(current_user: Annotated[User, Depends(get_current_user)]):
...原則:依存の戻り値をエンドポイント内で使わない場合は dependencies=[] リストに入れます。引数として宣言すると「なぜこの引数があるのか」が分かりにくくなります。
Annotated 型エイリアスによるコードの整理
Annotated を使うと、繰り返しのDI宣言をスッキリまとめられます。
# app/api/dependencies/database.py
from typing import Annotated, AsyncGenerator
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import AsyncSessionLocal
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with AsyncSessionLocal() as session:
yield session
# 型エイリアスとして定義
DbSession = Annotated[AsyncSession, Depends(get_db)]
CurrentUser = Annotated[User, Depends(get_current_user)]SQLAlchemy 2.x AsyncSession による非同期DB接続
非同期エンジンとセッションファクトリの正しい設定
# app/database.py
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
DATABASE_URL = "postgresql+asyncpg://user:pass@db:5432/mydb"
engine = create_async_engine(
DATABASE_URL,
echo=False, # 本番では False
pool_pre_ping=True, # 切断済みコネクションの自動検知
pool_size=10,
max_overflow=20,
)
AsyncSessionLocal = async_sessionmaker(
engine,
class_=AsyncSession,
expire_on_commit=False, # ← 最重要設定
)expire_on_commit=False が必須な理由【最頻出のハマりどころ】
デフォルト(expire_on_commit=True)では、session.commit() 後にすべてのORMオブジェクトが「期限切れ」状態になります。同期環境では次のアクセス時に自動で再ロードされますが、非同期環境ではこの遅延ロードがグリーンレットの外で実行されるためエラーになります。
sqlalchemy.exc.MissingGreenlet: greenlet_spawn has not been called;
can't call await_() here.
このエラーを見たら、まず expire_on_commit=False を確認してください。設定することで、コミット後もオブジェクトの値をそのまま使い続けられます。
yield 依存によるセッションのライフサイクル管理
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with AsyncSessionLocal() as session:
yield session
# with ブロック終了時に自動的にセッションをクローズ
# 例外発生時は自動ロールバックasync with コンテキストマネージャがセッションの開放とロールバックを保証します。明示的な try/finally は不要です。
Mapped Classes パターン(2.x推奨スタイル)
SQLAlchemy 2.xでは、Mapped 型と mapped_column() を使った型安全なモデル定義が推奨です。
# app/models/user.py
from datetime import datetime
from sqlalchemy import String, Boolean
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True, index=True)
email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
hashed_password: Mapped[str] = mapped_column(String(255))
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)旧来の Column(Integer, primary_key=True) 記法も動作しますが、型チェッカーとの親和性やコードの可読性の観点から、2.x記法への移行を強くお勧めします。
コミットはどの層で行うか
CRUDレイヤーでは session.commit() を呼ばないことを原則とします。
# crud/user_crud.py
async def create_user(db: AsyncSession, user_data: dict) -> User:
user = User(**user_data)
db.add(user)
await db.flush() # IDを取得するためflushのみ(コミットしない)
return user
# services/auth_service.py
async def register_user(db: AsyncSession, schema: UserCreate) -> User:
hashed = hash_password(schema.password)
user = await create_user(db, {"email": schema.email, "hashed_password": hashed})
await send_welcome_email(user.email) # 他の処理も同一トランザクション内
await db.commit() # サービス層でまとめてコミット
return userサービス層でコミットすることで、複数のCRUD操作を1トランザクションにまとめられます。
N+1問題と selectinload
非同期環境では遅延ロードが使えないため、関連データは必ずEager Loadingで取得します。
from sqlalchemy.orm import selectinload
result = await db.execute(
select(User).options(selectinload(User.items))
)
users = result.scalars().all()JWT認証 — アクセストークン+リフレッシュトークンの実装
なぜトークンを2種類に分けるのか
単一の長寿命トークン(例:7日有効)は実装が簡単ですが、漏洩時のリスクが大きく、失効手段がありません。2種類のトークンを組み合わせることでこの問題を解決します。
| アクセストークン | リフレッシュトークン | |
|---|---|---|
| 寿命 | 5〜15分 | 7〜30日 |
| 保管場所 | レスポンスボディ / メモリ | HttpOnly Cookie |
| DB保存 | 不要 | 必要(失効管理のため) |
| 失効可否 | 不可(寿命まで有効) | 可(DBから削除) |
JWTペイロード設計
# アクセストークンのペイロード例
{
"sub": "123", # ユーザーID
"type": "access", # トークン種別(必須)
"jti": "uuid-v4", # JWT ID(リフレッシュトークンで再利用検知に使用)
"scope": "read write", # 権限スコープ
"iss": "myapp", # 発行者
"aud": "myapp-api", # 対象オーディエンス
"exp": 1700000000, # 有効期限
}type クレームは特に重要です。これがないと、リフレッシュトークンをアクセストークンとして使う攻撃が成立してしまいます。
core/security.py の実装
# app/core/security.py
from datetime import datetime, timedelta, timezone
from typing import Any
import jwt
from passlib.context import CryptContext
from app.core.config import settings
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def hash_password(password: str) -> str:
return pwd_context.hash(password)
def verify_password(plain: str, hashed: str) -> bool:
return pwd_context.verify(plain, hashed)
def create_access_token(subject: str, extra: dict[str, Any] | None = None) -> str:
expire = datetime.now(timezone.utc) + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
payload = {
"sub": subject,
"type": "access",
"exp": expire,
"iss": settings.JWT_ISSUER,
"aud": settings.JWT_AUDIENCE,
**(extra or {}),
}
return jwt.encode(payload, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
def decode_access_token(token: str) -> dict | None:
try:
return jwt.decode(
token,
settings.SECRET_KEY,
algorithms=[settings.ALGORITHM],
audience=settings.JWT_AUDIENCE,
)
except jwt.InvalidTokenError:
return Noneリフレッシュトークンのローテーションと再利用検知
リフレッシュトークンを使用するたびに新しいトークンを発行し(ローテーション)、古いトークンをDB上で無効化します。使用済みのトークンが再度提示された場合はトークン盗難と判断して全セッションを失効させます。
# services/auth_service.py
async def refresh_tokens(db: AsyncSession, refresh_token: str) -> TokenPair:
payload = decode_refresh_token(refresh_token)
if payload is None:
raise HTTPException(status_code=401, detail="Invalid token")
jti = payload["jti"]
stored = await get_refresh_token_by_jti(db, jti)
if stored is None or stored.is_revoked:
# 再利用検知 → 全セッション失効
await revoke_all_user_tokens(db, int(payload["sub"]))
raise HTTPException(status_code=401, detail="Token reuse detected")
await revoke_refresh_token(db, jti) # 旧トークンを失効
new_access = create_access_token(payload["sub"])
new_refresh = await create_refresh_token(db, payload["sub"]) # 新トークン発行
await db.commit()
return TokenPair(access_token=new_access, refresh_token=new_refresh)認証まわりのアンチパターン
- ❌
localStorageにJWTを保存する:XSSで簡単に盗まれます。アクセストークンはメモリ管理、リフレッシュトークンはHttpOnly Cookieが原則です - ❌ シークレットキーをコードにハードコードする:環境変数か、本番ではシークレットマネージャを使います
- ❌
expクレームを検証しない:python-joseやPyJWTはexp検証をデフォルトで行いますが、オプションで無効化しないよう注意してください
Pydantic v2 によるリクエスト/レスポンス検証
v1からの主要な変更点
| v1 | v2 |
|---|---|
@validator |
@field_validator |
@root_validator |
@model_validator |
.dict() |
.model_dump() |
class Config: orm_mode = True |
model_config = ConfigDict(from_attributes=True) |
Optional[X] |
X | None |
model_validator による複合バリデーション
複数フィールドにまたがる検証が必要な場合は @model_validator(mode="after") を使います。
# app/schemas/user.py
from pydantic import BaseModel, EmailStr, field_validator, model_validator
class UserCreate(BaseModel):
email: EmailStr
password: str
password_confirm: str
@field_validator("password")
@classmethod
def password_strength(cls, v: str) -> str:
if len(v) < 8:
raise ValueError("パスワードは8文字以上必要です")
if not any(c.isupper() for c in v):
raise ValueError("大文字を1文字以上含めてください")
return v
@model_validator(mode="after") # モデル全体を受け取る
def passwords_match(self) -> "UserCreate":
if self.password != self.password_confirm:
raise ValueError("パスワードが一致しません")
return selfmode="after" では検証済みのモデルインスタンスを受け取るため、フィールド間の比較が安全に行えます。
Request/Responseスキーマの3分割
class UserCreate(BaseModel): # POST /users のリクエスト
email: EmailStr
password: str
password_confirm: str
class UserUpdate(BaseModel): # PATCH /users/{id} のリクエスト
email: EmailStr | None = None
is_active: bool | None = None
class UserResponse(BaseModel): # 全エンドポイントのレスポンス
id: int
email: str
is_active: bool
created_at: datetime
model_config = ConfigDict(from_attributes=True) # ORMオブジェクトから生成可能にpydantic-settings による設定管理
# app/core/config.py
from functools import lru_cache
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
DATABASE_URL: str
SECRET_KEY: str
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 15
REFRESH_TOKEN_EXPIRE_DAYS: int = 30
JWT_ISSUER: str = "myapp"
JWT_AUDIENCE: str = "myapp-api"
model_config = SettingsConfigDict(env_file=".env", case_sensitive=True)
@lru_cache
def get_settings() -> Settings:
return Settings()
settings = get_settings()@lru_cache により、Settings インスタンスはプロセス内で一度だけ生成されます。テスト時は app.core.config.get_settings をオーバーライドすることでテスト用設定に差し替えられます。
pytest-asyncio によるAPIエンドポイントテスト
基本セットアップ
# pyproject.toml
[tool.pytest.ini_options]
asyncio_mode = "auto"
# conftest.py
import pytest
from httpx import AsyncClient, ASGITransport
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
from app.main import app
from app.database import Base
from app.api.dependencies.database import get_db
TEST_DATABASE_URL = "sqlite+aiosqlite:///:memory:"
@pytest.fixture(scope="session")
async def engine():
eng = create_async_engine(TEST_DATABASE_URL)
async with eng.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield eng
async with eng.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
await eng.dispose()
@pytest.fixture
async def db_session(engine):
async_session = async_sessionmaker(engine, expire_on_commit=False)
async with async_session() as session:
yield session
await session.rollback() # テストごとにロールバックして独立性を保つ
@pytest.fixture
async def client(db_session):
async def override_get_db():
yield db_session
app.dependency_overrides[get_db] = override_get_db
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as c:
yield c
app.dependency_overrides.clear()dependency_overrides で差し替える
app.dependency_overrides は FastAPI が提供するDI差し替えの公式メカニズムです。本番の get_db をテスト用セッションに1行で入れ替えられます。
app.dependency_overrides[get_db] = override_get_db認証つきエンドポイントのテスト
@pytest.fixture
async def auth_client(client, db_session):
"""認証済みクライアントのフィクスチャ"""
# テストユーザーを作成してトークンを取得
response = await client.post("/auth/login", data={
"username": "test@example.com",
"password": "Testpassword1",
})
token = response.json()["access_token"]
client.headers["Authorization"] = f"Bearer {token}"
return client
async def test_get_items_requires_auth(client):
response = await client.get("/items/")
assert response.status_code == 401
async def test_get_items_authenticated(auth_client):
response = await auth_client.get("/items/")
assert response.status_code == 200uvicorn と gunicorn によるプロセス管理
uvicorn単体 vs gunicorn + UvicornWorker
| 環境 | 推奨構成 | 理由 |
|---|---|---|
| 開発 | uvicorn app.main:app --reload |
ホットリロードが使える |
| 本番(コンテナ + K8s) | uvicorn単体(複数レプリカで水平スケール) |
オーケストレータがプロセス管理を担う |
| 本番(単一VM) | gunicorn -k uvicorn.workers.UvicornWorker |
マルチプロセスでCPUを活用 |
gunicorn.conf.py の実践設定
# gunicorn.conf.py
import multiprocessing
# 非同期ワーカーの場合、CPUバウンドではないため (2×CPU+1) は過剰
# 1〜4程度から始めて負荷試験で調整する
workers = min(4, multiprocessing.cpu_count() * 2 + 1)
worker_class = "uvicorn.workers.UvicornWorker"
bind = "0.0.0.0:8000"
timeout = 60
graceful_timeout = 30
keepalive = 5
accesslog = "-"
errorlog = "-"
loglevel = "info"グレースフルシャットダウンの実装
# app/main.py
from contextlib import asynccontextmanager
from fastapi import FastAPI
from app.database import engine
@asynccontextmanager
async def lifespan(app: FastAPI):
# 起動時の処理(DB接続確認など)
yield
# シャットダウン時の処理
await engine.dispose() # コネクションプールを解放
app = FastAPI(lifespan=lifespan)lifespan イベントを使うことで、SIGTERMを受けた際にリクエスト処理完了→コネクションプール解放の順序が保証されます。
Dockerコンテナ化とヘルスチェック設計
マルチステージビルドによる軽量イメージ
# Dockerfile
FROM python:3.12-slim AS builder
WORKDIR /app
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev --no-install-project
FROM python:3.12-slim AS runtime
WORKDIR /app
COPY --from=builder /app/.venv /app/.venv
ENV PATH="/app/.venv/bin:$PATH"
# 非rootユーザーで実行(セキュリティ)
RUN useradd --create-home appuser
USER appuser
COPY --chown=appuser:appuser app/ ./app/
COPY gunicorn.conf.py ./
EXPOSE 8000
CMD ["gunicorn", "-c", "gunicorn.conf.py", "app.main:app"]
ヘルスチェックエンドポイントの3層設計
# app/api/routers/health.py
from fastapi import APIRouter, Depends
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.dependencies.database import get_db
router = APIRouter(prefix="/health", tags=["health"])
@router.get("/live")
async def liveness():
"""プロセスが生きているかのみ確認(Kubernetes liveness probe)"""
return {"status": "ok"}
@router.get("/ready")
async def readiness(db: AsyncSession = Depends(get_db)):
"""依存サービス(DB等)への疎通確認(Kubernetes readiness probe)"""
try:
await db.execute(text("SELECT 1"))
return {"status": "ok", "db": "connected"}
except Exception as e:
raise HTTPException(status_code=503, detail=f"DB unreachable: {e}")
@router.get("/startup")
async def startup():
"""起動完了確認(Kubernetes startup probe)"""
return {"status": "ok"}| プローブ | エンドポイント | 用途 |
|---|---|---|
| liveness | /health/live |
異常なら再起動 |
| readiness | /health/ready |
異常ならトラフィック停止 |
| startup | /health/startup |
起動完了まで他プローブを抑止 |
docker-compose.yml による開発環境
# docker-compose.yml
services:
app:
build: .
ports:
- "8000:8000"
env_file: .env
depends_on:
db:
condition: service_healthy
db:
image: postgres:16
environment:
POSTGRES_DB: mydb
POSTGRES_USER: user
POSTGRES_PASSWORD: password
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
interval: 5s
timeout: 5s
retries: 5
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:condition: service_healthy を使うことで、PostgreSQLが完全に起動してからアプリが接続を試みることを保証できます。
まとめ — 中級者が押さえるべき7つの原則
本記事で解説してきた内容を、実務で参照できるチェックリストとしてまとめます。
- [ ]
schemas/とmodels/を分離している:ORMモデルをそのままAPIレスポンスに使わない - [ ]
expire_on_commit=Falseを設定している:MissingGreenletエラーの回避 - [ ] コミットはサービス層で行っている:CRUDは
flushのみ、トランザクション境界を明確に - [ ] JWTに
typeクレームを含めている:リフレッシュトークンのアクセストークン偽用を防ぐ - [ ]
model_validator(mode="after")で複合バリデーションを実装している:フィールド間検証は@field_validatorではなく@model_validatorで - [ ]
dependency_overridesでテスト用DBに差し替えている:本番コードを変更せずにテスト環境を構築 - [ ] ヘルスチェックを3層に分けている:liveness / readiness / startup の役割を理解して使い分ける
次のステップ
本記事で紹介した構成をベースに、次のトピックに発展させることをお勧めします。
- ARQ/Celery:バックグラウンドタスクと非同期ジョブキュー
- レート制限:
slowapiを使ったエンドポイントごとのレート制御 - OpenTelemetry:分散トレーシングと可観測性の導入
- CI/CD:GitHub Actionsによるlint・テスト・Dockerビルドの自動化
付録:よくあるエラーと対処法
| エラー | 原因 | 対処法 |
|---|---|---|
MissingGreenlet: greenlet_spawn has not been called |
expire_on_commit=True 状態でコミット後にORMオブジェクトにアクセス |
async_sessionmaker に expire_on_commit=False を設定 |
greenlet_spawn has not been called (遅延ロード) |
非同期環境でリレーションを遅延ロードしようとした | selectinload / joinedload でEager Loadingを明示 |
422 Unprocessable Entity |
リクエストボディがPydanticスキーマにマッチしない | レスポンスの detail フィールドを確認し、型・必須フィールドをチェック |
DetachedInstanceError |
セッションが閉じた後にORMオブジェクトにアクセス | expire_on_commit=False 設定、またはセッション内でシリアライズを完了させる |
| アクセストークンでリフレッシュAPIが呼べる | JWTの type クレームを検証していない |
decode_refresh_token 内で payload["type"] == "refresh" を確認 |
関連記事
Gemini CLI ハンズオン:セットアップから実践活用まで完全ガイド【Claude Code・Codexとの比較付き】
Gemini CLI ハンズオン:セットアップから実践活用まで完全ガイド【Claude Code・Codexとの比較付き】 Gemini CLIとは?なぜ今注目されるのか Zenn・Qiitaを眺めると、Claude Codeに関する実践記事は急増しています。一方、GoogleのGemini CLIについては、セットアップ方法を紹介した記事こそあれど、「実際にコマンドを打ちながら学べる」ハンズオン...
Andrej Karpathy発「CLAUDE.md」4原則 — AIコーディングエージェントの品質を劇的に変えるルールファイル
Andrej Karpathy発「CLAUDE.md」4原則 — AIコーディングエージェントの品質を劇的に変えるルールファイル 「AIに書かせたコードが動くけど読めない」「頼んでいない箇所まで書き換えられた」「テストも実行もせずに"完了しました"と返ってくる」——コーディングエージェントを日常的に使うエンジニアであれば、これらの経験に身に覚えがあるはずです。生成AIへの期待が高まる一方で、こうし...
github-mcp-server 完全ガイド|GitHub公式MCPサーバーでAIエージェントにリポジトリ操作を任せる
github-mcp-server 完全ガイド|GitHub公式MCPサーバーでAIエージェントにリポジトリ操作を任せる > 本記事は2026年8月時点の情報をもとに執筆しています。github-mcp-serverは活発に開発が続いているため、最新情報は公式リポジトリをご確認ください。 --- はじめに:GitHub操作を「自然言語」で行える時代になった AIコーディングツールが普及した現在でも...
4BパラメータのオープンモデルがGPT-5.6 Solに並ぶ日 ― Castform × Neon Lakebaseで実現するRAGコスト1/100
4BパラメータのオープンモデルがGPT-5.6 Solに並ぶ日 ― Castform × Neon Lakebaseで実現するRAGコスト1/100 > メタディスクリプション: 40億パラメータのオープンモデルが、RL学習とエージェント型検索でGPT-5.6 Sol同等の精度を1/100のコストで達成。Castformのアーキテクチャ、Neon Lakebase Postgresの役割、コスト試...