雰囲気エンジニアの備忘録

Atmosphere Engineer's Memorandum

PostsFastAPIで型安全なAPIサーバーをサクッと構築する

FastAPIで型安全なAPIサーバーをサクッと構築する

概要

FastAPIは、PythonでAPIを構築するための非常に人気のあるフレームワークである。pydanticと組み合わせることで型安全性を提供し、開発者が迅速に高品質なAPIを構築できるように設計されている。本記事では、FastAPIを使用してラーメンのオーダーを管理するAPIサーバーを構築する方法を示す。

環境構築

FastAPIとその依存関係をインストールする。以下のコマンドを実行する。

pip install fastapi uvicorn pydantic

コード全体

app.py
from fastapi import FastAPI
from pydantic import BaseModel
from enum import Enum
from typing import List
import uuid

app = FastAPI()


@app.get("/healthz")
def check_healthy():
    return {"message": "This api is healty"}


# スキーマ定義
class Base(Enum):
    sio = "sio"  # 800
    miso = "miso"  # 850


class Size(Enum):
    namimori = "namimori"
    oomori = "oomori"
    tokumori = "tokumori"


class Topping(Enum):
    menma = "menma"
    nori = "nori"
    nitamago = "nitamago"
    tyashu = "tyashu"


# order
class OrderRequest(BaseModel):
    base: Base
    size: Size
    toppings: List[Topping]


class OrderResponse(OrderRequest):
    total_price: int


# 料金
BASE_PRICES = {
    Base.sio: 800,
    Base.miso: 850,
}

SIZE_PRICES = {
    Size.namimori: 0,
    Size.oomori: 50,
    Size.tokumori: 100,
}

TOPPING_PRICES = {
    Topping.menma: 30,
    Topping.nori: 30,
    Topping.nitamago: 50,
    Topping.tyashu: 80,
}

# ローカルのメモリ(テスト用)
ORDER_MEMORY = []


# api
@app.post("/orders")
def create_order(order: OrderRequest):
    # オーダーから合計金額を計算
    total = (
        BASE_PRICES[order.base]
        + SIZE_PRICES[order.size]
        + sum([TOPPING_PRICES[topping] for topping in order.toppings])
    )

    # オブジェクトを作成
    id = uuid.uuid4()
    order_response = OrderResponse(
        **order.dict(),
        uuid=id,
        total_price=total,
    )

    # メモリに追加
    ORDER_MEMORY.append(order_response)

    return {
        "message": "Order created successfully",
        "order": order_response,
    }


@app.get("/orders")
def get_order():
    return ORDER_MEMORY

解説

データモデルの定義

Pydanticを使用して、オーダーのリクエストとレスポンスのスキーマを定義する。
Enumを用いることで、定義されていないメンバ変数に対してErrorを発生させることができる。

from pydantic import BaseModel
from typing import List
from enum import Enum

class Base(Enum):
    sio = "sio"
    miso = "miso"

class Size(Enum):
    namimori = "namimori"
    oomori = "oomori"
    tokumori = "tokumori"

class Topping(Enum):
    menma = "menma"
    nori = "nori"
    nitamago = "nitamago"
    tyashu = "tyashu"

class OrderRequest(BaseModel):
    base: Base
    size: Size
    toppings: List[Topping]

class OrderResponse(OrderRequest):
    total_price: int

エンドポイントの定義

ヘルスチェックエンドポイント

APIの健康状態を確認するためのエンドポイントを作成する。

@app.get("/healthz")
def check_healthy():
    return {"message": "This api is healthy"}

オーダーエンドポイント

オーダーを作成するためのPOSTリクエストと、全オーダーを取得するためのGETリクエストを定義する。
今回は簡易的に、ORDER_MEORYという空のリストを定義して、メモリ上にデータを一時保管している。本番開発ではDBを使うのが良い。

# オーダー保存空間(テスト用) 
ORDER_MEMORY = []

@app.post("/orders")
def create_order(order: OrderRequest):
    # オーダーから合計金額を計算するロジック(後述)
    # ...

    ORDER_MEMORY.append(order_response)

    return {
        "message": "Order created successfully",
        "order": order_response,
    }

@app.get("/orders")
def get_order():
    return ORDER_MEMORY

合計金額計算ロジック

合計金額を計算するロジックを実装する。オーダーが与えられると、ベース、サイズ、トッピング料金の辞書をもとに合計を計算する。

# 料金
BASE_PRICES = {
    Base.sio: 800,
    Base.miso: 850,
}

SIZE_PRICES = {
    Size.namimori: 0,
    Size.oomori: 50,
    Size.tokumori: 100,
}

TOPPING_PRICES = {
    Topping.menma: 30,
    Topping.nori: 30,
    Topping.nitamago: 50,
    Topping.tyashu: 80,
}

# api
@app.post("/orders")
def create_order(order: OrderRequest):
    # オーダーから合計金額を計算
    total = (
        BASE_PRICES[order.base]
        + SIZE_PRICES[order.size]
        + sum([TOPPING_PRICES[topping] for topping in order.toppings])
    )
    # ...

実行

以下のコマンドでサーバーを起動する。

uvicorn main:app --reload

コマンドラインから通信テスト

curlコマンドで APIと通信できる。

# ヘルスチェック
curl localhost:8000/healthz

# レスポンス
# {"message":"This api is healty"}


# オーダー登録
curl -X 'POST' \
  'http://localhost:8000/orders' \ 
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "base": "miso",
  "size": "tokumori",
  "toppings": [
    "menma", "nitamago"
  ]
}' | jq

# レスポンス
# {
#   "message": "Order created successfully",
#   "order": {
#     "base": "miso",
#     "size": "tokumori",
#     "toppings": [
#       "menma",
#       "nitamago"
#     ],
#     "total_price": 1030   # ←合計料金が計算されている! 
#   }
# }

試しに、pydanticで定義した型ルールに反するリクエストを投げてみる。baseが醤油、toppingsにネギが足されており、これらはメニュー表にない。

以下のように、エラーに加えてどの定義が間違っているか教えてくれる。

curl -X 'POST' \
  'http://localhost:8000/orders' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "base": "shouyu",
  "size": "tokumori",
  "toppings": [
    "menma", "nitamago", "negi"
  ]
}' | jq


# レスポンス
# {
#   "detail": [
#     {
#       "type": "enum",
#       "loc": [
#         "body",
#         "base"
#       ],
#       "msg": "Input should be 'sio' or 'miso'",
#       "input": "shouyu",
#       "ctx": {
#         "expected": "'sio' or 'miso'"
#       }
#     },
#     {
#       "type": "enum",
#       "loc": [
#         "body",
#         "toppings",
#         2
#       ],
#       "msg": "Input should be 'menma', 'nori', 'nitamago' or 'tyashu'",
#       "input": "negi",
#       "ctx": {
#         "expected": "'menma', 'nori', 'nitamago' or 'tyashu'"
#       }
#     }
#   ]
# }

Swagger UIでの通信テスト

Fast APIにはSwagger UIというAPIのドキュメント化機能が組み込まれており、定義したエンドポイントの仕様書を表示し、APIをテストすることできる。

ブラウザでlocalhost:8000/docsアクセスする以下の画面が表示される。
Try it outのボタンから、適当にリクエストボディを編集してExecuteを実行すると、curlコマンドと同様の動作確認が行える。
swagger

まとめ

FastAPIを使用することで、型安全で高性能なAPIを簡単に構築することができた。