VISASQ Dev Blog

ビザスク開発ブログ

FastAPIの巨人の肩について調べました

こんにちは、暑い日が続きますね。
こんな日はライムがキリッと効いた一杯でキメたいところです、エキスパート/lite開発の下山です。

今回は自身が担当しているサービスのバックエンドであるFastAPIについてお話ししたいと思います。

FastAPIってなに

FastAPI は、Pythonの標準である型ヒントに基づいてPython 以降でAPI を構築するための、モダンで、高速(高パフォーマンス)な、Web フレームワークです。

fastAPI 公式ドキュメント

はい。

型ヒントあるし、モダンで高速なんですね。

ところでページ下部になんか書いてました。

FastAPI は巨人の肩の上に立っています。

・Web の部分はStarlette

・データの部分はPydantic

巨人の肩………………気になったのはもちろんWeb部分のStarletteです。

Starletteってなに

Starlette is a lightweight ASGI framework/toolkit, which is ideal for building async web services in Python.

Starlettは軽量なASGI framework/toolkitで、Pythonで非同期webサービスを構築するのに理想的です。(訳・下山)

Starlette 公式ドキュメント

つまり、ASGI (Asynchronous Server Gateway Interface) 、非同期処理による高い並行処理能力と様々な通信プロトコルのサポートという柔軟性を備えたフレームワーク/ツールキットってことですね。

これがFastAPIの高速(高パフォーマンス)を実現しているようです。

コードを見ていく

コードはFastAPIのチュートリアルから引用します。 importされたFastAPIクラスを掘っていくと、、、

from starlette.applications import Starlette
from starlette.datastructures import State
from starlette.exceptions import HTTPException
from starlette.middleware import Middleware
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.middleware.errors import ServerErrorMiddleware
from starlette.middleware.exceptions import ExceptionMiddleware
from starlette.requests import Request
from starlette.responses import HTMLResponse, JSONResponse, Response
from starlette.routing import BaseRoute
from starlette.types import ASGIApp, Lifespan, Receive, Scope, Send

class FastAPI(Starlette):

FastAPIクラス自体がStarletteを継承しています、この時点でStarletteの肩に乗る意味を理解。

ザッとクラスを整理すると、

はStarletteの機能、

  • 型ヒント
  • ドキュメント生成
  • データバリデーション(Pydantic)

はFastAPIの拡張機能のようです。

importから分かる通りStarletteは様々なミドルウェア機能が充実しています。既存のミドルウェアをそのまま使うもよし、既存のミドルウェアをカスタマイズして新しいミドルウェアを作るもよし。これらの機能をFastAPI上で利用できることが、高い拡張性や柔軟性を実現しているようです。早速ミドルウェアをカスタマイズして、レスポンスにカスタムヘッダを追加してみました、いやはや便利。

from fastapi import FastAPI, Request
from starlette.middleware.base import BaseHTTPMiddleware


class AddHeaderResponse(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        response = await call_next(request)
        response.headers["Hello_world"] = "(^o^)/~ thanks for reading"
        return response


app = FastAPI()

app.add_middleware(AddHeaderResponse)


@app.get("/")
async def root():
    return {"message": "Awesome FastAPI !!!"}

巨人の両肩

純粋にStarletteで以下のコードを実装します。

from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.requests import Request
from starlette.routing import Route

async def cook_curry(request: Request):
    curry = await request.json()
    return JSONResponse(curry)

async def root(request: Request):
    return JSONResponse({"message": "Hello, Starlette"})

routes = [
    Route("/", endpoint=root, methods=["GET"]),
    Route("/curry/", endpoint=cook_curry, methods=["POST"]),
]

app = Starlette(debug=True, routes=routes)

このコードでは/curry/エンドポイントに対してPOSTリクエストを送信する際に、リクエストボディをそのままレスポンスとして返します。リクエストはカレーであってほしいのですが、このままだとカレーじゃなくてシチューでも受け入れ可能です。では絶対にリクエストがカレーであることを担保したコードに修正してみます。

from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.requests import Request
from starlette.routing import Route
from pydantic import BaseModel, ValidationError
import json

class Curry(BaseModel): 
    spiciness: int
    topping: bool
    price: float


async def cook_curry(request: Request):
    try:
        # リクエストボディをJSONとしてパース
        body = await request.json()
        # Pydanticモデルを使ってバリデーションする
        curry = Curry(**body)
    except ValidationError as e:
        # バリデーションエラー時のレスポンス
        return JSONResponse(status_code=422, content=json.loads(e.json()))
    return JSONResponse(curry.dict())

(略)

リクエストボディがPydanticモデルでバリデーションされるよう修正しました。このようにStarlette自体にはデータバリデーションの機能は組み込まれておらず、開発者が独自にバリデーションのロジックを用意する必要があります。ここをFastAPIではもう1つの巨人の肩であるPydanticによって担保しています。

両肩が揃いました。

FastAPIだと以下のようになります、とってもシンプル。

from fastapi import FastAPI
from pydantic import BaseModel, ValidationError
from starlette.responses import JSONResponse
from starlette.requests import Request

class Curry(BaseModel):
    spiciness: int
    topping: bool
    price: float

app = FastAPI()

@app.post("/curry/")
async def cook_curry(menu: Curry):
    return menu

@app.get("/")
async def root():
    return {"message": "Hello, FastAPI!”}

終わりに

やっとFastAPIの巨人の肩の意味がわかりました。
次回はルーティング処理についてもっと詳しく調べてみたいと思います。

皆さんもぜひFastAPIを使って、効率的なAPI開発を体験してみてください!