0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

FastAPIで最初に知っておくとラクになるTips 6選

0
Posted at

FastAPI Logo

画像出典: FastAPI公式

FastAPIは、最初の「Hello World」まではかなりスムーズに進められるフレームワークです。

ただ、実際にAPIを何本か作り始めると、だんだん同じような処理が増えたり、レスポンス設計で迷ったり、認証やバリデーションの書き方が気になってきます。

そこでこの記事では、FastAPIを触り始めた段階で知っておくと、あとからかなり助かるTipsをコード例とともにまとめます。

特に、Depends、response_model、Pydantic、async / await、セキュリティまわりは、早めに押さえておくと設計がきれいになりやすいです。


Tip 1: リクエストボディは dict ではなく Pydantic モデルで受ける

FastAPIを使い始めたばかりだと、JSONをそのまま dict で受け取りたくなることがあります。

しかし、基本的にはPydanticモデルを使って専用のクラスを定義するほうが出口・入り口ともに圧倒的にラクになります。

FastAPIは、Pydanticモデルを使うことで、JSONの読み取り、型変換、データ検証、JSON Schema生成、自動ドキュメント(Swagger UI)への反映までまとめて処理してくれます。

❌ 避けたほうがいい例(dict で受ける)

from fastapi import FastAPI

app = FastAPI()

@app.post("/users")
def create_user(user_data: dict):
    # エディタの補完が効かず、キーの存在チェックや型変換を自前でする必要がある
    name = user_data.get("name")
    age = user_data.get("age")
    return {"status": "success", "name": name}

⭕ おすすめの例(Pydantic モデルで受ける)

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

# 受け取りたいデータ構造を定義
class UserCreate(BaseModel):
    name: str
    age: int
    email: str

@app.post("/users")
def create_user(user: UserCreate):
    # user.name や user.age で安全にアクセスでき、エディタの補完も完璧に効く
    return {"status": "success", "user": user}

この形にしておくと、受け取るデータの構造がコード上ではっきり見えるので、あとから見返したときも理解しやすいです。型が合わないリクエスト(例: age に文字列が送られてきた場合)は、FastAPIが自動で 422 Unprocessable Entity エラーを返してくれます。


Tip 2: バリデーションは自前で頑張りすぎない

APIの入り口で「文字数は〇文字以上で…」「数値は0以上で…」といったチェックを行う際、関数内で if 文を並べて手作業でバリデーションを書くのは避けましょう。コードが散らかり、自動ドキュメントにも仕様が反映されません。

FastAPIでは、Pydanticの Field や Pythonの Annotated を使うことで、宣言的(デコレータや型ヒントのようにつけるだけ)にバリデーションを記述できます。

❌ 避けたほうがいい例(関数内でif文チェック)

from fastapi import FastAPI, HTTPException

app = FastAPI()

@app.post("/items")
def create_item(name: str, price: float):
    if len(name) < 3:
        raise HTTPException(status_code=400, detail="名前は3文字以上にしてください")
    if price <= 0:
        raise HTTPException(status_code=400, detail="価格は0より大きい必要があります")
    return {"name": name, "price": price}

⭕ おすすめの例(モデル側で制限をかける)

from fastapi import FastAPI
from pydantic import BaseModel, Field
from typing import Annotated

app = FastAPI()

class ItemCreate(BaseModel):
    # Fieldを使って最小/最大文字数や、数値の範囲を制限する
    name: str = Field(..., min_length=3, max_length=50, description="商品名")
    price: float = Field(..., gt=0, description="価格(0より大きい値)")
    
    # 補足: 最近のFastAPIでは Pythonの Annotated を使った記述も推奨されています
    # name: Annotated[str, Field(min_length=3, max_length=50)]

@app.post("/items")
def create_item(item: ItemCreate):
    # この関数に入った時点で、データは完全に検証済み
    return item

このようにモデル側で表現する癖をつけると、エラーメッセージの形式が統一され、自動生成されるSwagger UIにも「3文字以上」といった制約が自動で明記されます。


Tip 3: response_model はかなり重要(または戻り値の型ヒント)

FastAPIを使うなら、「何をリクエストとして受け取るか」だけでなく「何をレスポンスとして返すか」を早くから意識することが重要です。

特に大事なのは、データベースの内部データをそのまま返さず、公開してよい形に絞り込む(フィルタリングする)ことです。パスワードハッシュや内部的なステータスコードを誤ってクライアントに返してしまう事故を防げます。

⭕ 記述例(response_model を使う、または戻り値の型ヒントを使う)

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

# データベース内部のデータ構造(パスワードを含む)
class UserInDB(BaseModel):
    id: int
    name: str
    email: str
    hashed_password: str

# クライアントに返却しても安全なデータ構造(パスワードを除外)
class UserPublic(BaseModel):
    id: int
    name: str
    email: str

@app.get("/users/{user_id}", response_model=UserPublic)
def get_user(user_id: int):
    # 疑似的にデータベースからデータを取得
    user_db = UserInDB(
        id=user_id,
        name="Taro",
        email="taro@example.com",
        hashed_password="super_secret_password_hash"
    )
    # response_model=UserPublic を指定しているため、
    # user_db をそのまま return しても自動的に hashed_password が削られます!
    return user_db

💡モダンなTips: > 最近のFastAPI(バージョン0.100以降など)では、response_model=UserPublic と書く代わりに、関数の戻り値の型ヒントとして def get_user(user_id: int) -> UserPublic: と書くだけでも同様のフィルタリングとドキュメント生成が行われます。お好みのスタイルで統一しましょう。

*画像出典: FastAPI response_model公式ドキュメント*


Tip 4: 共通処理は Depends に寄せる

FastAPIでコードが散らかり始めるポイントの1つが、共通処理の重複です。

たとえば、「ページネーションの共通パラメータ(skiplimit)」や「データベースのセッション取得」などを各APIにベタ書きし始めると、すぐにつらくなります。

そういうときに使いたいのが Depends(依存性注入)です。

❌ 避けたほうがいい例(共通処理のベタ書き)

@app.get("/items")
def read_items(skip: int = 0, limit: int = 10):
    return {"skip": skip, "limit": limit}

@app.get("/users")
def read_users(skip: int = 0, limit: int = 10):
    # 同じ引数とデフォルト値を何度も書く必要がある
    return {"skip": skip, "limit": limit}

⭕ おすすめの例(Depends を使った共通化)

from fastapi import FastAPI, Depends
from typing import Annotated

app = FastAPI()

# 共通処理を関数(またはクラス)として定義
def pagination_parameters(skip: int = 0, limit: int = 10):
    return {"skip": skip, "limit": limit}

@app.get("/items")
def read_items(commons: Annotated[dict, Depends(pagination_parameters)]):
    # commons には {"skip": X, "limit": Y} が注入される
    return {"message": "items", "params": commons}

@app.get("/users")
def read_users(commons: Annotated[dict, Depends(pagination_parameters)]):
    return {"message": "users", "params": commons}

最初のうちは少し回りくどく見えるかもしれませんが、「認証チェック」「DB接続の開始と自動クローズ(yieldを用いたクリーンアップ)」などを扱うようになると、この Depends が圧倒的な威力を発揮します。


Tip 5: async def と def は無理に統一しなくていい

FastAPIの大きな特徴は非同期処理(async/await)への対応ですが、「すべての関数を async def にしなければならない」というわけではありません。

判断基準はシンプルで、「関数の中で await する処理(非同期対応ライブラリ)があるかどうか」です。

import time
import asyncio
from fastapi import FastAPI

app = FastAPI()

# 1. 通常のデータベース(SQLAlchemyなど非同期非対応)や time.sleep を使う場合
# 普通の「def」で書けば、FastAPIがバックグラウンドのスレッドプールで安全に処理してくれます。
@app.get("/blocking")
def blocking_route():
    time.sleep(2)  # 重い処理
    return {"message": "Normal def"}

# 2. 非同期対応のライブラリ(HTTPX、Tortoise ORM、asyncpgなど)を使う場合
# 「async def」を使い、関数内で「await」を呼び出します。
@app.get("/async")
async def async_route():
    await asyncio.sleep(2)  # 非同期の重い処理
    return {"message": "Async def"}

よくあるミスとして、非同期非対応の重い処理を async def の中で普通に実行してしまうと、サーバー全体の処理がブロックされて(止まって)しまいます。迷ったら、まずは通常の def で書き、明確に非同期ライブラリを使うパートで async def を選択するのが安全です。


Tip 6: 認証まわりは自己流で始めすぎない

認証や認可(JWTトークンの検証など)は、セキュリティ事故を起こしやすい領域です。ヘッダーから自前で文字列をパースして…という実装を始めると、コードが複雑化します。

FastAPIには、標準でセキュリティスキームをサポートする fastapi.security モジュールが用意されています。これらを Depends と組み合わせることで、安全かつ標準的な認証が実装できます。

⭕ おすすめの例(HTTPBearer を使った簡易トークン認証)

from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from typing import Annotated

app = FastAPI()

# Bearerトークン(Authorization: Bearer <token>)を要求するスキーマ
security_scheme = HTTPBearer()

def get_current_user(credentials: Annotated[HTTPAuthorizationCredentials, Depends(security_scheme)]):
    token = credentials.credentials
    # 本来はここでJWTのデコードやDB照合を行う
    if token != "valid-secret-token":
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="無効なトークンです",
        )
    return {"username": "admin", "role": "developer"}

@app.get("/secure-data")
# 認証情報を依存関係として注入
def get_secure_data(current_user: Annotated[dict, Depends(get_current_user)]):
    return {"message": "認証に成功しました", "user": current_user}

この実装を行うだけで、Swagger UI上に「Authorize(鍵マーク)」のボタンが自動で出現し、ブラウザ上からトークンを入力してAPIのテストができるようになります。


FastAPIでありがちな初期ミスまとめ

ここまでの内容を振り返り、初期に陥りがちなアンチパターンをまとめます。

  1. リクエストを全部 dict で受ける
    → 型安全性が失われ、エディタ補完も効かなくなる。Pydanticモデルを使おう。
  2. レスポンス設計をせずに、DBのエンティティをそのまま返す
    → パスワードなどの機密情報漏洩リスク。response_model や型ヒントで絞り込もう。
  3. 共通処理(認証やパラメータ)を各エンドポイントにコピペする
    → 修正コストが肥大化する。Depends をフル活用しよう。
  4. async def を雰囲気で選ぶ
    → 同期処理を async def で動かすとパフォーマンス低下の原因に。ライブラリの仕様に合わせて使い分けよう。
  5. 認証を自己流で雑に実装する
    fastapi.security を使って、ドキュメント生成の恩恵も受けながら標準化しよう。

まとめ

FastAPIは、ただ動くAPIを作るだけなら非常に簡単なフレームワークです。しかし、実務で耐えうる綺麗なコードを保つためには、FastAPIが提供している強力な周辺エコシステム(PydanticやDepends)を正しく使う必要があります。

今回紹介した6つのTipsを意識するだけで、拡張性が高く、ドキュメントとコードが綺麗に同期した素晴らしいAPIを構築できるようになります。ぜひ日々の開発に取り入れてみてください!


参考リンク


お知らせ

以下は自分が出しているUdemy講座で、30日間使える半額クーポン付きリンクです。

FastAPIを体系的に学びたい方は、よければこちらもどうぞ。

Udemy講座はこちら

※価格やクーポンの適用状況は、Udemy側の表示をご確認ください。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?