Pythonの軽量なWebフレームワークとしてFlaskが有名でしたが、最近ではFastAPIのほうがよりモダンでドキュメントが豊富で使いやすいと聞いたので、FastAPIを使ってみました。
簡単なGETリクエストとJSONのリクエストボディを持つPOSTリクエストを受け取るAPIの作り方を紹介します
※ 基本的にFastAPIの公式ドキュメントに書いてある内容です(https://fastapi.tiangolo.com/ja/tutorial/)
- OS: Ubuntu20.04 (Windows11 WSL2)
- Python 3.8.10
- FastAPI 0.75.1
FastAPIのインストール
まず、開発に必要なパッケージをインストールしましょう。
fastapiとuvicornパッケージをインストールします。
pip install fastapi uvicorn
uvicornは、Python用のASGI(Asynchronous Server Gateway Interface)
サーバーの一つです。
ASGIはAsynchronous
と名前がつくように非同期処理を扱うことが可能です。
WebAPIを作るためのインターフェイスの総称としてWSGI(Web Server Gateway Interface)という単語がありますが、ASGIは非同期処理可能なWSGIと言えます。
ASGIとWSGIは似たような単語ですが、uvicornは非同期処理が可能なインターフェイスである、程度にとらえておけば大丈夫です。
GETリクエストを処理するAPIの作成
ここでは、単純なGETリクエストを処理するAPIと、クエリパラメータを持つリクエストを処理するAPIを作成してみます。
単純なGETリクエストを受け取るAPI
まず、単純なGETリクエストを受け取るAPIを作成してみます。
from fastapi import FastAPI
app = FastAPI()
@app.get("/hello")
async def root(): return {"message": "Hello World"}
上記のプログラムを実行してみましょう。実行コマンドは、uvicorn <ファイル名>:app
です。
$ uvicorn main:app --reload
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
※--reload
オプションはホットリロードを可能とするオプションです。
APIが立ち上がったので、リクエストを投げて動作確認してみましょう。
$ curl localhost:8000/hello
{"message":"Hello World"}
正常にAPIが起動しているようです。
リクエストパラメータを受け取るAPI
次に、クエリパラメータを持つGETリクエストを処理するAPIを作成してみます。
引数にクエリパラメータを追加するだけで実装可能です。
from fastapi import FastAPI
app = FastAPI()
@app.get("/hello")
async def hello(id: int=0, name: str=""): ret = { "id": id, 'name': name } return ret
上記のプログラムを実行して、動作確認してみましょう。
# APIの起動
$ uvicorn main:app --reload
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
$ curl "localhost:8000/hello?id=1&name=hoge"
{"id":1,"name":"hoge"}
しっかりとリクエストパラメータを処理できていることが確認できました。
ちなみに、リクエストパラメータにバリデーション処理を追加するのも簡単です。
気になる方は、公式ドキュメント(クエリパラメータと文字列の検証)を参考にしてください。
JSONのリクエストボディを持つPOSTリクエスト
次に、リクエストボディにJSONデータを持つPOSTリクエストを処理するAPIを作成していきます。
APIの作成
FastAPIには、JSONのリクエストボディを扱うためのクラスが実装されているので、それらを利用して作成していきます。
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Optional
app = FastAPI()
class Body(BaseModel): ver: int name: str opt: Optional[str] = None
@app.post("/hello")
async def sample(body: Body): return body
BaseModel
を継承してデータクラス(ここではBody
)を作成しています。
BaseModel
を継承することで、型変換・バリデーションチェックなどを自動で行ってくれます。
詳細はFastAPIのドキュメント(リクエストボディ)を参考ください。
動作確認
上記で作成したAPIを起動します。
$ uvicorn fastapi_get:app --reload
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
リクエストを投げてみます。
$ curl -X POST -H "Content-Type: application/json" -d '{"ver":1, "name":"hoge", "opt": "test"}' localhost:8000/hello
{"ver":1,"name":"hoge","opt":"test"}
POSTリクエストを正常に処理できていることが確認できますね。
ちなみに、上記で述べた通りBaseModel
はバリデーションを自動で行ってくれるので、Body
クラスで定義されたフィールドがリクエストに含まれていない場合は、エラーを返してくれます。
# リクエストから"name"フィールドを削除
$ curl -X POST -H "Content-Type: application/json" -d '{"ver":1}' localhost:8000/hello
{"detail":[{"loc":["body","name"],"msg":"field required","type":"value_error.missing"}]}
自動バリデーションは非常に便利ですね。
トラブルシューティング
FastAPIを利用してAPI開発するときに、よく遭遇するエラーとその回避策を紹介します。
Error loading ASGI app. Could not import module “XXX”
これは、実行時に指定するファイル名が異なっているときにでるエラーです。
実行時のコマンドとPythonファイル名が合っているかを確認しましょう。
# Pythonファイルがmain.pyの場合の実行コマンド
$ uvicorn main:app
# Pythonファイルがapi.pyの場合の実行コマンド
$ uvicorn api:app
# Pythonファイルがmain.pyでエラーがでる実行コマンド (正しくはuvicorn main:app)
$ uvicorn api:app
Error loading ASGI app. Could not import module "api"
307 Temporary Redirect
curl
コマンドなどでリクエストした際に307エラーが出る場合は、リクエストパスが異なります。
下記の実装の場合、正しいリクエストパスは/hello
であり、/hello/
ではありません。
# /helloの最後にスラッシュ"/"はない
@app.get("/hello")
async def root(): return {"message": "Hello World"}
上記のAPIの場合、正常系と異常系のリクエストは下記になります。
ポイントは最後のスラッシュ”/”です
# 正常系
$ curl "localhost:8000/hello"
{"message":"Hello World"}
# 異常系(307エラー)
$ curl "localhost:8000/hello/"
リクエストパスの末尾にスラッシュ”/”を含めたい場合は、ソースコードにもきちんと追加する必要があります。
# パスの末尾にスラッシュ"/"を追加すると、/hello/でリクエストを受け取る
@app.get("/hello/")
async def root(): return {"message": "Hello World"}
まとめ:FastAPIで簡単にWebAPIの作成が可能
FastAPIを使った簡単なWebAPIを作成してみましたが、Flask同様に非常に簡単にAPIを作成できました。
公式によるとFastAPIはFlaskより高速にリクエストを処理できるとのことですし、FastAPIは公式のドキュメントが非常に豊富です。
今すでにFlaskで作成済みのAPIをFastAPIに移行するメリットはあまりないかもしれませんが、今後新しくAPIを作成する際には、FlaskではなくFastAPIを使って実装するのはありだと感じました。