ChatGPT先生と挑むFastAPI #5|Router・Dependsと構成整理

記事内に広告が含まれています。
タイトル

これまでmain.pyにまとめて実装していたAPIを、APIRouterを使って機能ごとに分割します。

また、Dependsによる依存性注入の仕組みを学び、複数のAPIで利用する共通処理を整理します。

最後にPydanticモデルなども含めてファイル構成を見直し、実務を意識したFastAPIアプリの構成へ整理していきます。

ChatGPT先生
ChatGPT先生

それではSTEP5を始めましょう!

■■■スポンサーリンク■■■

今回学ぶこと

今回のステップで学ぶ内容を一覧で出してもらいました。

ChatGPT先生
ChatGPT先生

STEP5では以下の内容を学習します。

  1. APIRouterの役割と仕組みを理解する
  2. APIをRouterファイルへ分割する
  3. main.pyからRouterを登録して動作確認する
  4. Dependsと依存性注入の仕組みを理解する
  5. Dependsを使った共通処理を実装する
  6. schemas・routers・dependenciesを分けて構成を整理する
  7. 自力課題

学習開始

それでは学習をはじめます。

APIRouterの役割と仕組みを理解する

ChatGPT先生
ChatGPT先生

STEP 4までは、すべてのAPIをmain.pyへ直接書いていました。APIが数個ならこれでも問題ありません。

ただ、実務のアプリになると、

商品API
ユーザーAPI
注文API
認証API
...

というようにエンドポイントが増えていきます。全部main.pyに書き続けると、main.pyが巨大になってしまいます。

そこでFastAPIでは、APIRouterを使って関連するAPIをグループ化し、別ファイルへ分割できます。イメージとしては以下のような構成です。

main.py
   │
   ├── products.py
   │     ├── POST /products
   │     ├── GET /products/{id}
   │     ├── PUT /products/{id}
   │     └── DELETE /products/{id}
   │
   └── users.py
         ├── POST /users
         └── GET /users/{id}

appではなくrouterにAPIを登録する

ChatGPT先生
ChatGPT先生

これまで使ってきたのは、@app.get("/products")でした。

Routerファイルでは、まずAPIRouterを作成します。

from fastapi import APIRouter


router = APIRouter()

そして、以下のようにAPIを登録します。

@router.get("/products")
def get_products():
    ...
ChatGPT先生
ChatGPT先生

ただし、このrouterを作っただけではFastAPIアプリからは認識されません。

全体像としては次のようになります。

products.py

APIRouter
    ↓
商品APIを登録
    ↓
router
    │
    │ include_router()
    ↓
main.py
    ↓
FastAPI

DRFでいうと、完全に同じ仕組みではありませんが、views.py、urls.pyなどへ処理やURL定義を分けて、プロジェクト全体に取り込んでいく感覚に近いです。

FastAPIではAPIRouter自体が、「このグループには、こういうエンドポイントがあります」という情報を持ち、それをFastAPI本体へinclude_router()で登録します。

ここでまず押さえたいのは、APIRouterは、関連するAPIをグループ化して別ファイルへ分割し、FastAPIアプリへ組み込むための仕組みという点です。

APIをRouterファイルへ分割する

ChatGPT先生
ChatGPT先生

今回はまず、STEP 4で作った商品APIだけをmain.pyからRouterファイルへ移動します。

app配下にroutersフォルダを作成し、その中にproducts.pyを作ります。

app/
├── main.py
└── routers/
    └── products.py

今回のポイントは、いきなり構成を全部整理するのではなく、これまでmain.pyに書いていた商品APIを、APIRouterへ移すところだけです。

app/routers/products.pyに次のように記述してください。

記述するコード↓

from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, Field


router = APIRouter()


class ProductCreate(BaseModel):
    name: str
    price: int = Field(gt=0)
    description: str | None = None


class ProductUpdate(BaseModel):
    name: str
    price: int = Field(gt=0)
    description: str | None = None


products = {
    1: {
        "name": "ノートPC",
        "price": 120000,
        "description": "開発用PC",
    },
    2: {
        "name": "マウス",
        "price": 3000,
        "description": None,
    },
}

ここまでは、STEP 4でmain.pyに書いていた商品API用のデータやPydanticモデルを移しただけです。
そして、商品に関するCRUDのデコレータを、

@app.post(...)
@app.get(...)
@app.put(...)
@app.delete(...)

から、

@router.post(...)
@router.get(...)
@router.put(...)
@router.delete(...)

へ変更して、商品APIの各関数もすべてproducts.pyへ移してください。

ここではまだmain.pyからRouterを登録しないので、この時点では商品APIが動かなくても正常です。

修正したproducts.py↓

from fastapi import APIRouter, HTTPException, status
from pydantic import BaseModel, Field

router = APIRouter()


class ProductCreate(BaseModel):
    name: str
    price: int = Field(gt=0)
    description: str | None = None


class ProductUpdate(BaseModel):
    name: str
    price: int = Field(gt=0)
    description: str | None = None


products = {
    1: {
        "name": "ノートPC",
        "price": 120000,
        "description": "開発用PC",
    },
    2: {
        "name": "マウス",
        "price": 3000,
        "description": None,
    },
}


@router.post(
    "/products",
    status_code=status.HTTP_201_CREATED,
)
def create_product(product: ProductCreate):
    # 現在の商品IDの最大値から、新しい商品IDを作成する
    new_product_id = max(products.keys(), default=0) + 1

    # Pydanticモデルを辞書に変換する
    product_data = product.model_dump()

    # 新しい商品を保存する
    products[new_product_id] = product_data

    # 作成された商品IDと商品データを返す
    return {
        "id": new_product_id,
        **product_data,
    }


@router.get(
    "/products/{product_id}",
    responses={
        404: {
            "detail": "Product not found",
        }
    }
)
def get_product(product_id: int):
    # 指定された商品IDの商品を取得する
    product = products.get(product_id)

    # 商品が存在しない場合は404エラーを返す
    if product is None:
        raise HTTPException(
            status_code=404,
            detail="Product not found",
        )

    return product


@router.put("/products/{product_id}")
def update_product(product_id: int, product: ProductUpdate):
    # 商品が存在しない場合は404エラーを返す
    if product_id not in products:
        raise HTTPException(
            status_code=404,
            detail="Product not found",
        )

    # 指定された商品IDのデータを更新する
    products[product_id] = product.model_dump()

    return products[product_id]


@router.delete("/products/{product_id}")
def delete_product(product_id: int):
    # 商品が存在しない場合は404エラーを返す
    if product_id not in products:
        raise HTTPException(
            status_code=404,
            detail="Product not found",
        )

    # 指定された商品IDのデータを削除する
    deleted_product = products.pop(product_id)

    return deleted_product

このコードをChatGPT先生に見せました。

ChatGPT先生
ChatGPT先生

問題ありません。今回の目的である「商品APIをmain.pyからAPIRouterへ移す」ことは正しくできています。

1点だけ、Routerとは直接関係ありませんが、以前からあるこの部分は、

responses={
    404: {
        "detail": "Product not found",
    }
}

OpenAPI上のレスポンス説明として書くなら、後で

responses={
    404: {
        "description": "Product not found",
    }
}

のようにする方が適切です。ここは今回のRouter学習とは別なので、今すぐ深掘りする必要はありません。

1点指摘がありました。STEP4でChatGPT先生が提示したコードをそのまま貼り付けていた部分だったのですが、descriptionの方が適切だそうです。

ChatGPT先生
ChatGPT先生

このresponsesは、Swagger UI / OpenAPIドキュメントに「404の場合はこういうレスポンスです」と説明を追加するための設定です。FastAPI公式ドキュメントでも、追加レスポンスの説明にはdescriptionを使っています。

一方、実際に404エラーを返す以下は、detailで正しいです。HTTPExceptionのdetailは、クライアントへ返すJSONレスポンスのdetailフィールドに入る値だからです。

raise HTTPException(
    status_code=404,
    detail="Product not found",
)

つまり整理すると、以下のようになります。

responses={404: {"description": "..."}}
                    ↑
          APIドキュメント上の説明

HTTPException(detail="...")
                  ↑
          実際のエラーレスポンス

とのことでしたので、products.pyを修正しました。

main.pyからRouterを登録して動作確認する

ChatGPT先生
ChatGPT先生

現在のproducts.pyには商品APIがありますが、この時点では、以下のところまでしかできていません。

products.py
    ↓
APIRouterに商品APIを登録

FastAPI本体であるappは、まだこのrouterの存在を知りません。
そこでmain.pyから登録します。

main.pyのimport部分へ追加してください。

from app.routers.products import router as products_router

ここでは、routerという名前のままimportすると、今後Routerが増えたときに何のRouterなのか分かりにくくなるため、products_routerという名前にしています。

そして、app = FastAPI()の後に次のコードを追加します。

app.include_router(products_router)

何が起きているのか

ChatGPT先生
ChatGPT先生

products.pyでは、router = APIRouter()に以下を登録しました。

POST   /products
GET    /products/{product_id}
PUT    /products/{product_id}
DELETE /products/{product_id}

そのrouterを、app.include_router(products_router)でFastAPI本体へ組み込んでいます。

イメージすると、以下のような関係です。

products.py
┌─────────────────────────┐
│ products_router                                  │
│                                                  │
│ POST   /products                                 │
│ GET    /products/{id}                            │
│ PUT    /products/{id}                            │
│ DELETE /products/{id}                            │
└───────────┬─────────────┘
                        │
                        │ include_router()
                        ▼
main.py
┌─────────────────────────┐
│ FastAPI app                                      │
└─────────────────────────┘

動作確認

ChatGPT先生
ChatGPT先生

以下の4つがこれまでと同じようにSwagger UIへ表示されているか確認します。

POST   /products
GET    /products/{product_id}
PUT    /products/{product_id}
DELETE /products/{product_id}

さらに、GET /products/1を実行して以下のレスポンスが返ってくればOKです。

{
  "name": "ノートPC",
  "price": 120000,
  "description": "開発用PC"
}

各エンドポイントがSwagger UIに表示されていることを確認↓

Swagger UIの表示結果

GET /products/1の実行結果↓

GETリクエストの結果

Dependsと依存性注入の仕組みを理解する

ChatGPT先生
ChatGPT先生

ここからSTEP 5のもう一つの中心であるDependsに入ります。

まずコードを書く前に、「何のために使うのか」を押さえましょう。

APIには共通処理が増えてくる

ChatGPT先生
ChatGPT先生

たとえば今後、複数のAPIで次のような処理が必要になることがあります。

  • ログインユーザーを取得する
  • APIキーをチェックする
  • DBセッションを取得する
  • 共通のクエリパラメータを処理する

仮に、複数のAPIすべてで認証チェックが必要だとします。
これを普通に書くと、以下のようになり同じ処理が何度も登場します。

def get_product():
    # 認証チェック
    ...

    # 商品取得処理
    ...


def create_product():
    # 認証チェック
    ...

    # 商品登録処理
    ...


def update_product():
    # 認証チェック
    ...

    # 商品更新処理
    ...

共通処理を関数にすれば、以下のように書けます。

def check_auth():
    ...


def get_product():
    check_auth()
    ...


def create_product():
    check_auth()
    ...

もちろんPythonとしてはこれでも動きます。

FastAPIでは、このような「APIの処理を実行するために必要となる処理や値」をDependsを使って宣言できます。

Dependsの基本形

ChatGPT先生
ChatGPT先生

たとえば、次のような共通処理があるとします。

from fastapi import Depends


def get_common_value():
    return "共通の値"

Dependsを使ってAPI側では次のように書けます。

@router.get("/example")
def example(
    common_value: str = Depends(get_common_value),
):
    return {"value": common_value}

処理順は、以下のようになります。

リクエスト
   ↓
FastAPI
   ↓
Depends(get_common_value)
   ↓
get_common_value() を実行
   ↓
戻り値を common_value に渡す
   ↓
example() を実行

なぜ「依存性注入」と呼ぶのか

ChatGPT先生
ChatGPT先生

少し名前が難しいのですが、ここは言葉を分解すると分かりやすいです。

example()は、common_valueという値を必要としています。つまりexample()はcommon_valueに依存していると考えます。

しかし、example()自身が、common_value = get_common_value()と取得するのではありません。

代わりに、Depends(get_common_value)と宣言しておくと、FastAPIが必要な値を用意して関数へ渡してくれます。

この「外側から必要なものを渡す」という考え方が依存性注入(Dependency Injection / DI)です。

<自分で取得する>
API → get_common_value()を直接呼ぶ

<Dependsを使う>
API ← FastAPIが必要な値を渡す

という違いがあります。

Depends()は単なる関数呼び出しではない

ChatGPT先生
ChatGPT先生

ここは重要です。

Depends(get_common_value)とget_common_value()は意味が違います。

get_common_value()なら、その場所で普通に関数を呼び出しています。

一方、Depends(get_common_value)は、「このAPIはget_common_valueに依存しています。この処理はFastAPIに任せます」という宣言です。

FastAPIがその依存関係を解決してから、API関数を実行します。

実務では何に使う?

ChatGPT先生
ChatGPT先生

FastAPIで特によく見るのは、Depends(get_db)のようなDB接続・DBセッションの取得や、Depends(get_current_user)のような認証済みユーザーの取得です。

STEP 6でSQLAlchemyを扱うようになると、DependsとDBセッションを組み合わせる形も出てきます。

なので今回のSTEP 5では、まず簡単な共通処理でDependsそのものの仕組みを理解するところまでやります。

DRF経験と結び付けるなら、認証や権限チェックなど「各Viewが必要とする共通機能」をフレームワーク側の仕組みに乗せる、という発想には近い部分があります。ただしFastAPIのDependsは認証専用ではなく、任意の依存処理を宣言できる汎用的な仕組みです。

ここで押さえておきたいのは、Dependsは、APIが必要とする共通処理や値をFastAPIに解決してもらい、API関数へ渡すための仕組みという点です。

Dependsを使った共通処理を実装する

ChatGPT先生
ChatGPT先生

今回やりたいことは、商品一覧APIで使う以下のクエリパラメータの取得処理を、API本体から切り離して共通処理として扱うことです。

limit
offset

今回の完成形は次の形です。

完成形のコード↓

from typing import Annotated

from fastapi import APIRouter, Depends, HTTPException, status


router = APIRouter()


def get_pagination(limit: int = 10, offset: int = 0):
    # 一覧取得で使用するページング条件をまとめて返す
    return {
        "limit": limit,
        "offset": offset,
    }


@router.get("/products")
def get_products(
    pagination: Annotated[dict, Depends(get_pagination)],
):
    # ページング条件を取得する
    limit = pagination["limit"]
    offset = pagination["offset"]

    # 商品IDを含めた一覧データを作成する
    product_list = [
        {
            "id": product_id,
            **product,
        }
        for product_id, product in products.items()
    ]

    # offsetの位置からlimit件の商品を返す
    return product_list[offset : offset + limit]

① get_pagination()は何をしているのか

ChatGPT先生
ChatGPT先生

まずここです。これは普通のPython関数です。

def get_pagination(limit: int = 10, offset: int = 0):
    return {
        "limit": limit,
        "offset": offset,
    }

ただしFastAPIから見ると、以下はパスパラメータではないのでクエリパラメータとして扱われます。

limit: int = 10
offset: int = 0

つまり、GET /products?limit=5&offset=10というリクエストが来た場合、FastAPIはこの関数に対して、

get_pagination(
    limit=5,
    offset=10,
)

相当の値を渡してくれます。そしてこの関数は以下を返します。

{
    "limit": 5,
    "offset": 10,
}

② Depends(get_pagination)は何を意味しているのか

ChatGPT先生
ChatGPT先生

商品一覧APIではこう書いています。

pagination: Annotated[dict, Depends(get_pagination)]

ここを分解すると、次のようになります。

Annotated[
    dict,
    Depends(get_pagination),
]

まず、dictはpaginationには辞書型の値が入るというPythonの型情報です。

そして、Depends(get_pagination)はFastAPIに対して、paginationの値を用意するときは、get_paginationを依存処理として使ってくださいと伝えています。

つまり、get_products()自身が、get_pagination()を直接呼んでいるわけではありません。FastAPIが代わりに呼び出します。

③ 実際のリクエスト時の処理順

ChatGPT先生
ChatGPT先生

GET /products?limit=1&offset=1のリクエストが来た時の処理の流れはこうです。

GET /products?limit=1&offset=1
        ↓
FastAPI
        ↓
Depends(get_pagination) を確認
        ↓
get_pagination(limit, offset) の引数をリクエストから解決
        ↓
get_pagination() を実行
        ↓
戻り値を pagination に渡す
        ↓
get_products() を実行

④ get_products()側は依存処理の結果だけを使う

ChatGPT先生
ChatGPT先生

get_products()では、次のようにしています。

limit = pagination["limit"]
offset = pagination["offset"]

この時点ではもう、「クエリパラメータをどこから取得するか」、「デフォルト値はいくつか」といったことをget_products()側で考える必要がありません。

get_products()は、ページング条件はpaginationに入ってくるという前提で、自分の商品一覧取得処理だけに集中できます。

これがDependsを使うメリットの一つです。

⑤ なぜこれが「共通処理」になるのか

ChatGPT先生
ChatGPT先生

今回はget_products()からしか使っていません。でも今後、@router.get("/users")や@router.get("/orders")でも同じページング条件を使いたくなった場合、それぞれで、

limit: int = 10
offset: int = 0

を書く代わりに、

pagination: Annotated[dict, Depends(get_pagination)]

と書けば、同じ処理を再利用できます。

つまり、処理の役割を次のように分けられています。

ページング条件を取得する責務:get_pagination()
商品一覧を取得する責務:get_products()

⑥ Annotatedを使う理由

ChatGPT先生
ChatGPT先生

以下の書き方でもFastAPIとしては動きます。

pagination: dict = Depends(get_pagination)

ただ、

pagination: Annotated[dict, Depends(get_pagination)]

とすることで、以下を1つの型アノテーションとしてまとめられます。

dict:Pythonとしての型情報
Depends(get_pagination):FastAPIに対する依存関係の追加情報

最初の書き方では、今回使っているRuffのB008警告が発生してしまいますが、Annotatedを使用することで警告を回避できます。

⑦一番理解してほしい部分

ChatGPT先生
ChatGPT先生

今回の項目5で一番理解してほしいのは、次の部分です。

Depends(get_pagination)と書くと、get_products()がget_pagination()を直接呼ぶのではなく、FastAPIが依存関係を解決して、戻り値をpaginationへ渡してくれる。

そして、

get_pagination()の引数もFastAPIが解析するため、limitとoffsetがクエリパラメータとして扱われる。

の2点です。

動作確認

ChatGPT先生
ChatGPT先生

Swagger UIでGET /productsが追加されていることを確認し、次の2パターンを試してみてください。

<パターン1>
limit = 1
offset = 0

<パターン2>
limit = 1
offset = 1

パターン1の実行結果↓

パターン1の実行結果

パターン2の実行結果↓

パターン2の実行結果
ChatGPT先生
ChatGPT先生

問題ありません。

リクエスト
  ↓
get_pagination()
  ↓
pagination
  ↓
get_products()

というDependsの依存関係が想定どおり動いています。

schemas・routers・dependenciesを分けて構成を整理する

ChatGPT先生
ChatGPT先生

ここでは、今のproducts.pyに集まっているものを役割ごとに分離します。

現在はざっくり、以下のようになっています。

products.py
├── ProductCreate
├── ProductUpdate
├── products
├── get_pagination()
└── 商品API

これを今回は、次の構成に整理します。

app/
├── main.py
├── dependencies.py
├── routers/
│   └── products.py
└── schemas/
     └── products.py

それぞれの役割はこう考えます。

ファイル役割
main.pyFastAPIアプリ本体とRouterの登録
routers/products.py商品APIのエンドポイント
schemas/products.py商品APIで使うPydanticモデル
dependencies.py複数のAPIから利用できる依存処理

小規模なAPIなら同じファイルでも問題ありませんが、実務で規模が大きくなるなら、Pydanticモデルをschemasとして分離する構成はよく使われます。

こうすると、次のように責務が分かりやすくなります。

APIの入口       → routers
データの形      → schemas
共通の依存処理 → dependencies

schemasを分離する

ChatGPT先生
ChatGPT先生

一気に全部変更すると何を移したのか分かりづらくなるので、まずPydanticモデルだけ移しましょう。

app/schemas/products.pyを作成してください。

from pydantic import BaseModel, Field


class ProductCreate(BaseModel):
    name: str
    price: int = Field(gt=0)
    description: str | None = None


class ProductUpdate(BaseModel):
    name: str
    price: int = Field(gt=0)
    description: str | None = None

そして、現在のapp/routers/products.pyから、以下を削除します。

class ProductCreate(BaseModel):
    ...


class ProductUpdate(BaseModel):
    ...

代わりに以下をimportします。

from app.schemas.products import ProductCreate, ProductUpdate

その結果、routers/products.pyではもう、以下は不要になるので削除します。

from pydantic import BaseModel, Field

つまりRouter側から見ると、「リクエストのデータ構造についてはschemas側から借りてくる」という状態になります。

dependencies.pyへ分離する

ChatGPT先生
ChatGPT先生

次は、現在routers/products.pyに残っているこの処理を移します。

def get_pagination(limit: int = 10, offset: int = 0):
    # 一覧取得で使用するページング条件をまとめて返す
    return {
        "limit": limit,
        "offset": offset,
    }

これは商品固有の処理というより、一覧取得で共通利用できるページング条件を作る処理なので、Routerから切り離してdependencies.pyへ置いてみます。

現在の構成にapp/dependencies.pyを追加してください。

app/
├── main.py
├── dependencies.py   ← 追加
├── routers/
│   └── products.py
└── schemas/
     └── products.py

dependencies.pyには、先ほどの関数をそのまま移します。

そしてrouters/products.pyからget_pagination()を削除して、代わりにimportします。

from app.dependencies import get_pagination

つまりRouter側では、次のような形になります。

from typing import Annotated

from fastapi import APIRouter, Depends, HTTPException, status

from app.dependencies import get_pagination
from app.schemas.products import ProductCreate, ProductUpdate

Dependsの部分は変更しません。ここがポイントで、依存処理を別ファイルへ移しても、Dependsの仕組み自体は何も変わりません。

routers/products.py
        │
        │ Depends(get_pagination)
        ▼
dependencies.py
        │
        │ get_pagination()
        ▼
limit / offset

という関係になります。これで、

APIの入口           → routers
データ構造          → schemas
共通の依存処理      → dependencies

まで責務を分離できます。今回の整理によって、

routers
    → APIのエンドポイント・処理

schemas
    → リクエストなどのデータ構造

dependencies
    → APIから利用する共通の依存処理

main.py
    → FastAPIアプリの作成・Routerの登録

という構成になりました。

これは今回のSTEPでかなり重要なポイントです。単にファイルを分けたのではなく、責務ごとに分離したと考えてください。

動作確認

ChatGPT先生
ChatGPT先生

Swagger UIから、GET /products?limit=1&offset=1を実行して、「マウス」が返ればOKです。

実行結果↓

実行結果
ChatGPT先生
ChatGPT先生

確認できました。GET /products?limit=1&offset=1が200で成功し、id: 2の「マウス」が返っています。

つまり、get_pagination()をdependencies.pyへ移した後も、

リクエスト
  ↓
Depends(get_pagination)
  ↓
dependencies.py の get_pagination()
  ↓
pagination
  ↓
routers/products.py の get_products()

という依存関係が正常に動いています。

自力課題

ChatGPT先生
ChatGPT先生

今回はSTEP 4の自力課題で作成したBook CRUD APIを題材にしましょう。

課題内容

ChatGPT先生
ChatGPT先生

現在main.pyにあるBook関連の処理を、商品APIと同じ考え方で整理してください。

完成時の構成は、次のイメージです。

app/
├── main.py
├── dependencies.py
├── routers/
│   ├── products.py
│   └── books.py
└── schemas/
     ├── products.py
     └── books.py

今回、自力で対応してほしい内容は次の4点です。

  1. Book APIをrouters/books.pyへ移す
  2. Book用のPydanticモデルをschemas/books.pyへ移す
  3. main.pyからBook Routerを登録する
  4. Swagger UIからBook APIがこれまでどおり動作することを確認する

さらに、Book一覧APIがある場合は、get_paginationをDependsで利用して、商品一覧と同じようにlimitとoffsetを利用できるようにしてみてください。

完了条件

ChatGPT先生
ChatGPT先生

Swagger UI上で商品APIとBook APIの両方が表示され、BookのCRUDが正常に動作すること。

また、main.pyにはBookのCRUD処理やPydanticモデルを直接書かず、FastAPIアプリの作成とRouter登録が中心になっている状態を目指してください。

実装結果

課題対応後のmain.py↓

from fastapi import FastAPI
from pydantic import BaseModel, Field

from app.routers.books import router as books_router
from app.routers.products import router as products_router


class UserCreate(BaseModel):
    name: str
    age: int = Field(gt=0)
    email: str | None = None


# FastAPIアプリケーションを作成する
app = FastAPI()

# 商品APIのRouterをFastAPIアプリへ登録する
app.include_router(products_router)
# 書籍APIのRouterをFastAPIアプリへ登録する
app.include_router(books_router)


# GET / にアクセスされたときの処理を定義する
@app.get("/")
def read_root():
    return {"message": "Hello, FastAPI!"}


@app.get("/hello")
def read_hello():
    return {"message": "Hello from my first API!"}


@app.get("/users/{user_id}")
def get_user(user_id: int):
    return {
        "user_id": user_id,
    }


@app.get("/users")
def get_users(limit: int | None = None):
    return {
        "limit": limit,
    }


@app.get("/users/{user_id}/items")
def get_user_items(
    user_id: int,
    limit: int = 10,
):
    return {
        "user_id": user_id,
        "limit": limit,
    }


@app.post("/users")
def create_user(user: UserCreate):
    # リクエストボディから受け取ったユーザーデータをそのまま返す
    return user

routers/books.py↓

from typing import Annotated

from fastapi import APIRouter, Depends, HTTPException, status

from app.dependencies import get_pagination
from app.schemas.books import BookCreate, BookUpdate

router = APIRouter()

books = {}


@router.get("/books")
def get_books(
    pagination: Annotated[dict, Depends(get_pagination)],
):
    # ページング条件を取得する
    limit = pagination["limit"]
    offset = pagination["offset"]

    # 書籍IDを含めた一覧データを作成する
    book_list = [
        {
            "id": book_id,
            **book,
        }
        for book_id, book in books.items()
    ]

    # offsetの位置からlimit件の書籍を返す
    return book_list[offset : offset + limit]


@router.post(
    "/books",
    status_code=status.HTTP_201_CREATED,
)
def create_book(book: BookCreate):
    # 現在の書籍IDの最大値から、新しい書籍IDを作成する
    new_book_id = max(books.keys(), default=0) + 1

    # Pydanticモデルを辞書に変換する
    book_data = book.model_dump()

    # 新しい書籍を保存する
    books[new_book_id] = book_data

    # 作成された書籍IDと書籍データを返す
    return {
        "id": new_book_id,
        **book_data,
    }


@router.get("/books/{book_id}")
def get_book(book_id: int):
    book = books.get(book_id)

    # 書籍が存在しない場合は404エラーを返す
    if book is None:
        raise HTTPException(
            status_code=404,
            detail="Book not found",
        )

    return book


@router.put("/books/{book_id}")
def update_book(book_id: int, book: BookUpdate):
    # 書籍が存在しない場合は404エラーを返す
    if book_id not in books:
        raise HTTPException(
            status_code=404,
            detail="Book not found",
        )

    # 指定された書籍IDのデータを更新する
    books[book_id] = book.model_dump()

    return books[book_id]


@router.delete("/books/{book_id}")
def delete_book(book_id: int):
    # 書籍が存在しない場合は404エラーを返す
    if book_id not in books:
        raise HTTPException(
            status_code=404,
            detail="Book not found",
        )

    # 指定された書籍IDのデータを削除する
    deleted_book = books.pop(book_id)

    return deleted_book


@router.get("/books/{book_id}/reviews")
def get_book_reviews(
    book_id: int,
    limit: int = 10,
    offset: int | None = None
):
    return {
        "book_id": book_id,
        "limit": limit,
        "offset": offset,
    }

schemas/books.py↓

from pydantic import BaseModel, Field


class BookCreate(BaseModel):
    title: str
    price: int = Field(gt=0)
    description: str | None = None


# STEP4 自力課題 書籍更新用
class BookUpdate(BaseModel):
    title: str
    price: int = Field(gt=0)
    description: str | None = None

Swagger UI上で商品APIとBook APIの両方が表示されていることを確認↓

SwaggerUIの表示内容

以下の順にAPIを実行して動作確認していきます。

  1. POST /booksで書籍を2件登録
  2. GET /books?limit=1&offset=1で2件目だけ取得できることを確認
  3. GET /books/{book_id}で登録した書籍を取得
  4. PUT /books/{book_id}で更新
  5. DELETE /books/{book_id}で削除
  6. 削除したIDをGETして404になることを確認

POSTリクエストの結果(2件登録)↓

POSTの結果

GET /books?limit=1&offset=1で2件目だけ取得した結果↓

GETリクエストの結果

id:1の書籍を取得した結果↓

IDを指定してGETした結果

id:1の書籍を更新した結果↓

PUTの結果

id:1の書籍を削除した結果↓

DELETEの結果

削除したIDを取得した結果↓

削除したIDを取得した結果

ChatGPT先生のレビュー結果

ChatGPT先生
ChatGPT先生

確認できました。自力課題は問題なく完了です。

今回の自力課題では、APIRouterによる分割、schemasへのPydanticモデル分離、Dependsの再利用、main.pyからのRouter登録まで自力で組み合わせて実装できています。

まとめ

今回はschemas・routers・dependenciesに分けて構成を整理しました。

schemas:APIで扱うデータ構造をPydanticモデルとして定義
routers:APIのエンドポイントと、その入口となる処理を定義
dependencies:Dependsで利用する依存処理・依存値を定義

ただ単にファイルを分割するのではなく、それぞれの責務を考えて分割することが重要であると学びました。

実務で扱う上で、可読性や保守性を高めるために、きちんと責務で分離してきれいな構成を作ることはかなり大事になりそうだと感じました。しっかり理解して使いこなせるようにしたいです。

APIRouterについて学んだことの整理
  • APIRouterに機能ごと(書籍、商品、ユーザーなど)にAPIを登録する
  • APIRouterは各機能のAPIをグループ化したようなイメージ
    (書籍用のAPIRouter、商品用のAPIRouter、ユーザー用のAPIRouter)
  • APIRouterにAPIを登録しただけでは、FastAPIアプリ本体にはまだそのAPIが登録されていない
  • include_router()でAPIRouterをFastAPI本体のルーティングへ組み込む。
  • これにより、APIRouterに登録したAPIがFastAPIアプリから利用できるようになる。
Dependsについて学んだことの整理
  • APIの処理を実行するために必要な処理や値を、Dependsを使って依存関係として宣言できる
  • 今回の学習の内容の場合、リクエストが来たら以下のような順序で処理が実施される。
リクエスト
↓
FastAPIが Depends(get_pagination) を確認
↓
get_pagination(limit, offset) の引数をリクエストから解決
↓
get_pagination() を実行
↓
戻り値を pagination に渡す
↓
get_books() / get_products() を実行
  • 単に共通処理だから分離するというだけでなく、ページングの処理をAPIから切り離すことで、本来のAPIの機能に集中でき、責務を分離することができる

正直、現時点ではまだ理解しきれていないため、しっかりと理解して使いこなせるように復習したいと思います。

コメント

タイトルとURLをコピーしました