見出し画像

FastAPIを使って簡単なREST APIを実装する方法をまとめてみた


はじめに


こんにちは、株式会社SHIFT DAAE(ダーエ)テクノロジーグループのイケモトです。今回はFastAPIを使って簡単なREST APIを実装する方法をまとめてみました。

FastAPIとは、Python 3.6以上で非同期WebアプリケーションおよびAPI開発のためのWebフレームワークです。FastAPIの特徴として、公式のドキュメントでは以下の通りに挙げられています。

  • 高速 : Node.js や Go言語に匹敵する高速なアプリケーションを開発できる 。Pythonフレームワークの中では最も高速。

  • 高速なコーディング:開発速度を約 200%~300%向上させる。

  • 少ないバグ : 開発者起因のヒューマンエラーを約 40%削減する。

  • 直感的: エディタへのサポートが充実しており、どこでも自動補完が機能する。

  • 簡単: 簡単に利用、習得できるようにデザインされており、ドキュメントを読む時間を削減する。

  • 短い: コードの重複を最小限にしており、パラメータ宣言から複数の機能を呼ぶことができ、バグも少ない。

  • 堅牢性:本番環境でも利用できるコードが実装でき、ドキュメントも自動的に生成される。

  • 準拠: API のオープンスタンダードに基づいており、OpenAPI (以前は Swagger として知られていました) や JSON スキーマと完全に互換性があります。

FastAPIは公式ドキュメントの内容が充実していますが、その反面、使い始めるだけのために参照するにはボリュームがあり、少し辛いと感じました。

そこで、自分が初めてFastAPIに触れた際の経験を元に、覚えておきたい使い方をまとめています。 FastAPIに初めて触れる方や、私のように普段API開発にあまり関わらない方の参考になれば幸いです。

構築環境


各項目の簡単な説明

項目別URL:Poetry FastAPI Uvicorn

前提


  • FastAPIを利用するためにPython3.6以上が必要です。事前にPython3.6以上をインストールしてください。

  • DBに接続する方法は今回取り扱いません。FastAPIを使ったDBへの接続に関して知りたい場合は公式ドキュメントのこちら を確認してください。

サーバー構築までの手順


Poetryのインストール

以下のコマンドを実行してPoetryをインストールします。

brew install poetry

FastAPIとUvicornのインストール

適当な階層にディレクトリを作ります。

mkdir sampleMock
cd sampleMock

以下のコマンドを叩いて初期化します。

poetry init

色々と聞かれますが、全てReturnキーで返してしまって大丈夫です。
フォルダの中にpyproject.tomlが作成されたら完了です。

以下のコマンドを実行して、FastAPIとUvicornをインストールします。

$  poetry add fastapi uvicorn

これでインストールは完了です。

ローカルサーバーを起動して動作を確認する

アプリケーションの作成

main.pyを作成して、以下のように記述してください。

# main.py

# FastAPIのインポート.
# FastAPIは、APIのすべての機能を提供するPythonクラスです
from fastapi import FastAPI

# FastAPIの「インスタンス」を生成
# このappという名前はuvicornを使ってサーバーを起動する際に参照します。
app = FastAPI()

# パスとHTTPメソッドを指定します
# 直下の関数がリクエストの処理を担当します
@app.get("/")
def root():
    return {"mock": "Hello World"}

uvicornを使ってサーバーを起動する

以下のコマンドを実行して、仮想環境に入ります。

poetry shell

仮想環境に入れたら、続いて以下のコマンドを実行してモックサーバーを起動します。

uvicorn main:app --reload

以下のcurlコマンドを実行すると、先ほど設定したJSONレスポンスを確認できると思います。

curl http://127.0.0.1:8000
{"mock": "Hello World"}

パスパラメーターとクエリを扱えるようにする


パスパラメーターの設定

Pythonの文字列フォーマットと同様のシンタックスを用いることで、パスパラメーターの宣言ができます。

# main.py
from fastapi import FastAPI

app = FastAPI()

@app.get("/shop/{shop_id}")
def read_shop(shop_id : str):
    return {"shop_id": shop_id, "type": "hoge_type", "name":"fuga_shop"}

パスパラメーターshop_idの値が引数shop_id としてメソッドに渡されます。

以下のコマンドを実行すると設定したJSONレスポンスが返ってくることが確認できます。

curl http://127.0.0.1:8000/shop/piyo
{"shop_id": piyo, "type": "hoge_type", "name":"fuga_shop"}

また、shop_idは変数として扱えるため、分岐条件を用いることで動的にレスポンスを変更することも可能です。

# main.py
from fastapi import FastAPI

app = FastAPI()

@app.get("/shop/{shop_id}")
def read_shop(shop_id : str):
    if(shop_id == "piyo") : 
        return {"shop_id": shop_id, "type": "hoge_type", "name":"fuga_shop"}
    else :
        return {"shop_id": shop_id, "type": "hogera_type", "name":"fugara_shop"}
curl http://127.0.0.1:8000/shop/piyopiyo
{"shop_id": piyopiyo, "type": "hogera_type", "name":"fugara_shop"}

クエリパラメーターの設定

パスパラメーターに存在しない引数を設定すると、FastAPIでは自動的にクエリパラメーターとして扱われます。

# main.py
from fastapi import FastAPI

app = FastAPI()

@app.get("/shop/{shop_id}")
def read_shop(shop_id : str):
    if(shop_id == "piyo") : 
        return {"shop_id": shop_id, "type": "hoge_type", "name":"fuga_shop"}
    else :
        return {"shop_id": shop_id, "type": "hogera_type", "name":"fugara_shop"}

@app.get("/goods")
def read_goods(goods_id:str):
    if (goods_id == "hoge"):
        return {"name" : "pencil", "price" : "120"}
    else:
        return {"name" : "note", "price" : "200"}

クエリパラメータを変更するとgoods_idの値に応じて返却されるJSONレスポンスが変わることが確認できます。

curl -G -d goods_id=hoge http://127.0.0.1:8000/goods
{"name":"pencil","price":"120"}
curl  http://127.0.0.1:8000/goods?goods_id=fuga
{"name":"note","price":"200"}

ここで注意しなければいけないのは、宣言したクエリパラメーターにデフォルト値が設定されていない場合、必須のクエリパラメーターとして扱われます
試しに必須のクエリパラメーターを設定していない状態でcurlコマンドを実行するとエラーが返ってきます。

curl  http://127.0.0.1:8000/goods
{"detail":[{"type":"missing","loc":["query","goods_id"],"msg":"Field required","input":null}]}

オプショナルなクエリパラメーターとして宣言したい場合は、引数のデフォルト値をNoneにする必要があります。

# main.py
from fastapi import FastAPI
from typing import Union

app = FastAPI()

@app.get("/shop/{shop_id}")
def read_shop(shop_id : str):
    if(shop_id == "piyo") : 
        return {"shop_id": shop_id, "type": "hoge_type", "name":"fuga_shop"}
    else :
        return {"shop_id": shop_id, "type": "hogera_type", "name":"fugara_shop"}

@app.get("/goods")
def read_goods(goods_id: Union[str, None] = None):
    if(goods_id):
        if (goods_id == "hoge"):
            return {"name" : "pencil", "price" : "120"}
        else:
            return {"name" : "note", "price" : "200"}
    
    return {"name" : "eraser", "price" : "140"}

先ほどエラーが返ってきたcurlコマンドを再度実行すると、クエリパラメータがなかった場合でもJSONレスポンスが返ってくることが確認できます。

curl  http://127.0.0.1:8000/goods
{"name":"eraser","price":"140"}

複数のファイルを使ってモックを構築する。


Routersを使って複数ファイルに切り分ける

Routersを使うことで、今までmain.pyにまとめていたAPIを複数のファイルに切り分けることができます。 まずは、read_shop関数とread_goods関数をそれぞれ別のファイルに移します。

#shop.py
from fastapi import APIRouter

# appの代わりにrouterをインスタンスする
router = APIRouter()

# app → routerに置き換える
@router.get("/shop/{shop_id}")
def read_shop(shop_id : str):
    if(shop_id == "piyo") : 
        return {"shop_id": shop_id, "type": "hoge_type", "name":"fuga_shop"}
    else :
        return {shop_id: shop_id, "type": "hogera_type", "name":"fugara_shop"}
#goods.py
from fastapi import APIRouter
from typing import Union

# appの代わりにrouterをインスタンスする
router = APIRouter()

# app → routerに置き換える
@router.get("/goods")
def read_goods(goods_id: Union[str, None] = None):
    if(goods_id):
        if (goods_id == "hoge"):
            return {"name" : "pencil", "price" : "120"}
        else:
            return {"name" : "note", "price" : "200"}
    
    return {"name" : "eraser", "price" : "140"}

次にmain.pyを以下のように書き換えます。

# main.py
from fastapi import FastAPI

# 各ファイルからrouterをインポートする
from shop import router as shop_router
from goods import router as goods_router

app = FastAPI()

# app.include_routerを使って各ファイルからインポートしたrouterを追加する。
app.include_router(shop_router)
app.include_router(goods_router)

各ファイルに切り分けた後でも、今までアクセスしたURLから同じJSONレスポンスが返ってくることが確認できます。

リクエストボディを受け取れるようにする。


JSONを受け取る場合

JSON形式のリクエストボディを扱うためには、PydanticのBaseModelを使う必要があります。
まずはpoertyを使って依存関係を追加します。

poetry add pydantic

Pydanticを追加できたら、shop.pyを以下のように書き変えます。

# goods.py
from fastapi import APIRouter
from typing import Union
# pydanticをインポートする
from pydantic import BaseModel

router = APIRouter()

# データモデルを作成する。
class Goods(BaseModel):
    goods_id : str
    name: str
    price: float

@router.get("/goods")
def read_goods(goods_id: Union[str, None] = None):
    if(goods_id):
        if (goods_id == "hoge"):
            return {"name" : "pencil", "price" : "120"}
        else:
            return {"name" : "note", "price" : "200"}
    
    return {"name" : "eraser", "price" : "140"}

# リクエストボディを返却するPOSTを実装する
@router.post("/goods/create")
def create_good(goods : Goods):
    return goods

以下のcurlコマンドを実行すると、入力したリクエストボディがそのまま返ってきます。

curl -X POST http://127.0.0.1:8000/goods/create -H 'Content-Type: application/json' -d '{ "goods_id": "abcde", "name": "ruler","price": 220 }'
{"goods_id":"abcde","name":"ruler","price":220}

フィールドを受け取る場合

JSONの代わりにフィールドを受け取る場合、FastAPIのFormクラスを使います。
create_goodメソッドがフォームを受け取れるように、先程のコードを以下のように変更します。

# goods.py
# Formをインポートする
from fastapi import APIRouter, Form
from typing import Union

router = APIRouter()

@router.get("/goods")
def read_goods(goods_id: Union[str, None] = None):
    if(goods_id):
        if (goods_id == "hoge"):
            return {"name" : "pencil", "price" : "120"}
        else:
            return {"name" : "note", "price" : "200"}
    
    return {"name" : "eraser", "price" : "140"}

# フォームデータを返却するPOSTを実装する
# 引数にフォームパラメーターを実装する
@router.post("/goods/create")
def create_good(goods_id : str = Form(), name : str = Form(), price : float = Form()):
    return {"goods_id" : goods_id, "name" : name, "price": price}

以下のcurlコマンドを実行すると、入力したフォームデータがそのまま反映されていることが確認できます。

curl -X POST http://127.0.0.1:8000/goods/create -d 'goods_id=abcde&name=ruler&price=220'
{"goods_id":"abcde","name":"ruler","price":220.0}

カスタムレスポンスを返す


FastAPIはデフォルトではJSONResponseクラスを使ってJSON形式のレスポンスを返します。
この挙動をオーバーライドするためには、Responseクラスを使う必要があります。
以下はResponseクラスを使ってJSON形式のレスポンスの代わりにXML形式のレスポンスを返すコードになります。

# customer.py
from fastapi import APIRouter
# Responseをインポートする
from fastapi.responses import Response
from typing import Union

router = APIRouter()

@router.get("/customer")
def get_customer():
    xml_content = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
    <doc>
    <customer_name "SHIFT 太郎">
    <customer_age "19">
    </doc>
    """

    # encodeを利用することで指定した文字コードに変換することも可能
    xml_content_sjis = xml_content.encode("shift_jis")

    # Responseクラスの引数media_typeを指定して、xml形式のレスポンスを返す。
    return Response(xml_content_sjis, media_type="application/xml; charset=Shift_JIS")

customer.pyの実装が終わったら忘れずにmain.pyにrouterを追加しましょう。

#main.py
from fastapi import FastAPI
from shop import router as shop_router
from goods import router as goods_router
# customer.pyからrouterをインポートする
from customer import router as customer_router

app = FastAPI()

app.include_router(shop_router)
app.include_router(goods_router)
# routerを追加する。
app.include_router(customer_router)

curlコマンドを実行すると、xml形式でレスポンスが返ってくることが確認できます。

curl http://127.0.0.1:8000/customer
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
    <doc>
    <customer_name "SHIFT 太郎">
    <customer_age "19">
    </doc>

おわりに


今回はFastAPIを使って簡単なREST APIを実装する方法をまとめてみました。 このブログの内容を参考に使い始めてみて、追加で対応したい要件を公式ドキュメントで探していくと、実装が進めやすいかと思います。

私自身も普段はあまりAPI開発を行うことはないのですが、今回の案件内でモックサーバーを構築するために初めてFastAPIに触れました。当初はPrismを使ってモックサーバーを構築していましたが、開発を進めるにつれてモックサーバーの要件としてレスポンスデータを動的に変更する必要が生じました。Prismを使うだけではその要件を満たすことが難しく、代わりにFastAPIを使ってAPIモックを行うことになりました。

このように、非同期WebアプリケーションやAPI開発以外にもFastAPIを活用する場合があるので、普段API開発などを行わない方でもFastAPIの使い方を覚えておくと役に立つ場面があるかもしれません。

参考サイト


\もっと身近にもっとリアルに!DAAE 公式 X/


執筆者プロフィール:Ikemoto Fumito
Unity・C#でのゲーム開発やKotlin・JavaでのAndroidアプリ実装を主に行ってました。
最近はまたAndroidに帰ってきました。

お問合せはお気軽に
https://service.shiftinc.jp/contact/

SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/

SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/

SHIFTの導入事例
https://service.shiftinc.jp/case/

お役立ち資料はこちら
https://service.shiftinc.jp/resources/

SHIFTの採用情報はこちら
https://recruit.shiftinc.jp/career/

PHOTO:UnsplashMarkus Spiske