はじめに
データ処理系の学習が一段落したので、次はWeb API。
Python案件でAPIを書く機会があるかもしれないということで、FastAPIを触ってみた。PythonのWebフレームワークはDjango・Flask・FastAPIが主要どころで、「API開発ならFastAPIが今のスタンダード」という意見が多かったのでこれを選んだ。
LaravelでAPIを作り慣れていたので、バリデーション・ルーティング・DI周りをLaravelと比べながら理解した。思想がかなり違う部分もあり、そこが面白かった。
インストール
pip install fastapi uvicorn
uvicornはASGIサーバー。LaravelでいうApache/Nginxのような立ち位置だが、開発サーバーとしても使える。
# 開発サーバーを起動
uvicorn main:app --reload
# main → ファイル名(main.py)
# app → FastAPIインスタンスの変数名
# --reload → ファイル変更時に自動リロード
最初のAPI
# main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"message": "Hello, World!"}
@app.get("/users/{user_id}")
def read_user(user_id: int):
return {"user_id": user_id, "name": "田中"}
これだけで動く。Laravelと比べると圧倒的に少ない。
<?php
// Laravel
Route::get('/', function () {
return response()->json(["message" => "Hello, World!"]);
});
Route::get('/users/{user_id}', [UserController::class, "show"]);
Laravelはルート定義とコントローラーが分離しているが、FastAPIはデコレータでルートと処理を直接紐づける。Flaskに近い感覚。
起動後にhttp://localhost:8000/docsにアクセスするとSwagger UIが自動生成される。これは最初に触れたとき素直に感動した。コードを書くだけでAPIドキュメントが出来上がる。
ルーティング
from fastapi import FastAPI
app = FastAPI()
# GETリクエスト
@app.get("/items")
def list_items():
return [{"id": 1}, {"id": 2}]
# POSTリクエスト
@app.post("/items")
def create_item():
return {"message": "作成しました"}
# PUTリクエスト
@app.put("/items/{item_id}")
def update_item(item_id: int):
return {"item_id": item_id}
# DELETEリクエスト
@app.delete("/items/{item_id}")
def delete_item(item_id: int):
return {"message": "削除しました"}
パスパラメータ
@app.get("/users/{user_id}/posts/{post_id}")
def get_user_post(user_id: int, post_id: int):
return {"user_id": user_id, "post_id": post_id}
パスパラメータは関数の引数に型ヒントをつけるだけで、自動でバリデーションとキャストが行われる。
GET /users/abc/posts/1
→ 422 Unprocessable Entity(user_idがintでないためエラー)
LaravelだとRoute::get('/users/{user_id}', ...)と$request->route('user_id')で受け取り、型チェックは自前でやる必要があった。FastAPIは型ヒントを書くだけで済む。
クエリパラメータ
@app.get("/items")
def list_items(
page: int = 1,
per_page: int = 20,
keyword: str | None = None,
):
return {
"page": page,
"per_page": per_page,
"keyword": keyword,
}
パスパラメータ以外の引数は自動でクエリパラメータとして扱われる。
GET /items?page=2&per_page=10&keyword=python
→ {"page": 2, "per_page": 10, "keyword": "python"}
デフォルト値をつければ省略可能、Noneで省略可能かつNull許容。Laravelの$request->query('page', 1)に相当するものが引数定義だけで書ける。
Pydantic — リクエストボディのバリデーション
ここがFastAPIの一番の特徴といってもいい。
Laravelでリクエストのバリデーションをする場合。
<?php
// Laravel FormRequest
class StoreUserRequest extends FormRequest
{
public function rules(): array
{
return [
"name" => ["required", "string", "max:50"],
"age" => ["required", "integer", "min:0", "max:150"],
"email" => ["required", "email"],
];
}
}
FastAPIではPydanticのモデルを使う。
from fastapi import FastAPI
from pydantic import BaseModel, Field, EmailStr
app = FastAPI()
class UserCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=50)
age: int = Field(..., ge=0, le=150)
email: EmailStr
@app.post("/users")
def create_user(user: UserCreate):
# バリデーション済みのデータが user に入っている
return {"name": user.name, "email": user.email}
# バリデーションエラーのレスポンス(自動生成)
POST /users
{"name": "", "age": -1, "email": "invalid"}
→ 422 Unprocessable Entity
{
"detail": [
{"loc": ["body", "name"], "msg": "String should have at least 1 character"},
{"loc": ["body", "age"], "msg": "Input should be greater than or equal to 0"},
{"loc": ["body", "email"],"msg": "value is not a valid email address"}
]
}
型ヒントを書くだけでバリデーションとエラーレスポンスが自動で用意される。EmailStrを使うにはpydantic[email]が必要。
pip install pydantic[email]
レスポンスモデルの定義
from pydantic import BaseModel
class UserResponse(BaseModel):
id: int
name: str
email: str
# パスワードなど返したくないフィールドは含めない
class UserCreate(BaseModel):
name: str
email: str
password: str
@app.post("/users", response_model=UserResponse)
def create_user(user: UserCreate):
# DBに保存する処理(省略)
saved_user = {"id": 1, "name": user.name, "email": user.email, "password": user.password}
return saved_user
# response_modelによってpasswordは自動的に除外される
response_modelを指定するとレスポンスのフィルタリングとドキュメント生成が自動で行われる。Laravelのリソースクラスに近い役割。
依存性注入(DI)
FastAPIにはシンプルなDIの仕組みが組み込まれている。
共通処理をDependsで注入する
from fastapi import FastAPI, Depends, HTTPException, Header
app = FastAPI()
# 認証処理を共通化
def verify_token(authorization: str = Header(...)):
if authorization != "Bearer secret-token":
raise HTTPException(status_code=401, detail="認証エラー")
return authorization
@app.get("/protected")
def protected_route(token: str = Depends(verify_token)):
return {"message": "認証済みです"}
Depends()に関数を渡すと、その関数の返り値が引数に注入される。認証・DBセッション・ページネーションなど共通処理を切り出してどのエンドポイントにも適用できる。
DBセッションのDI(よくあるパターン)
from fastapi import Depends
# DBセッションを提供するdependency
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
@app.get("/users/{user_id}")
def get_user(user_id: int, db = Depends(get_db)):
user = db.query(User).filter(User.id == user_id).first()
if user is None:
raise HTTPException(status_code=404, detail="ユーザーが見つかりません")
return user
yieldを使うとリクエスト処理後にクリーンアップが走る。LaravelのServiceContainerとは思想が違うが「共通処理を関数として渡す」というシンプルな設計で直感的だった。
エラーハンドリング
from fastapi import FastAPI, HTTPException
app = FastAPI()
@app.get("/users/{user_id}")
def get_user(user_id: int):
if user_id <= 0:
raise HTTPException(
status_code=400,
detail="user_idは1以上を指定してください",
)
if user_id > 100:
raise HTTPException(
status_code=404,
detail="ユーザーが見つかりません",
)
return {"user_id": user_id}
HTTPExceptionをraiseするとそのままJSONエラーレスポンスになる。
カスタム例外ハンドラー
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
app = FastAPI()
class DomainError(Exception):
def __init__(self, message: str, code: int = 400):
self.message = message
self.code = code
@app.exception_handler(DomainError)
async def domain_error_handler(request: Request, exc: DomainError):
return JSONResponse(
status_code=exc.code,
content={"error": exc.message},
)
@app.get("/items/{item_id}")
def get_item(item_id: int):
if item_id < 0:
raise DomainError("IDは0以上を指定してください")
return {"item_id": item_id}
LaravelのHandler.phpでカスタム例外を処理するのと同じことがexception_handlerデコレータで書ける。
ルーターで分割する
エンドポイントが増えたらファイルを分割する。
myapi/
├── main.py
└── routers/
├── __init__.py
├── users.py
└── items.py
# routers/users.py
from fastapi import APIRouter
router = APIRouter(prefix="/users", tags=["users"])
@router.get("/")
def list_users():
return [{"id": 1, "name": "田中"}]
@router.get("/{user_id}")
def get_user(user_id: int):
return {"user_id": user_id}
@router.post("/")
def create_user():
return {"message": "作成しました"}
# main.py
from fastapi import FastAPI
from routers import users, items
app = FastAPI()
app.include_router(users.router)
app.include_router(items.router)
APIRouterはLaravelのRoute::group()に相当する。prefixでURLのプレフィックスを、tagsでSwagger UIのグループ名を設定できる。
LaravelとFastAPIの比較
| 項目 | Laravel | FastAPI |
|---|---|---|
| ルーティング | routes/api.php |
デコレータ(@app.get) |
| コントローラー | クラス必須 | 関数でOK |
| バリデーション | FormRequest / Validator | Pydanticモデル |
| レスポンス整形 | APIResource | response_model |
| DI | ServiceContainer | Depends() |
| 認証 | Sanctum / Passport |
Depends()で自作 or ライブラリ |
| ORMデフォルト | Eloquent | なし(SQLAlchemyが一般的) |
| APIドキュメント | 別途Swaggerを設定 | 自動生成(/docs) |
| 非同期 | 基本同期(Octane使用) | ネイティブ対応(async/await) |
Laravelはフルスタックフレームワークでほぼ全部入りだが、FastAPIはAPIに特化していてシンプル。ORMや認証は別途選んで組み合わせる設計。
まとめ
- デコレータでルートと処理を直接紐づける
- 型ヒントを書くだけでバリデーション・キャスト・ドキュメントが自動で生成される
- Pydanticモデルでリクエスト/レスポンスの型が明示できる
-
Depends()でDIを実現する -
/docsでSwagger UIが自動で使える
「型ヒントを書くだけで多くのことが自動化される」という設計思想が一貫していて、書いていて気持ちよかった。Laravelほど「何でもある」ではないが、その分シンプルで学習コストが低い。