当サイトは、アフィリエイト広告を利用しています

【Docker × FastApi × Poetry】REST APIを実装する~基本~

作成日:2024月09月16日
更新日:2024年09月19日

PythonのフレームワークであるFastAPIの開発環境を
dockerを使ってコンテナで構築し、VScodeからコンテナ内で
FastAPI製の基本的なREST APIを実装、動作させる方法をまとめる

この記事では基本となる

  • VSCodeでの開発環境構築
  • Poetryでの依存関係管理
  • REST API実装
  • uvicornサーバー起動
  • 動作確認
  • デバッグ方法
  • ドキュメントの自動生成

についてを実際に手順を追って解説していく

パッケージ管理について

Pythonのパッケージ管理はpipではなくPoetryを使って行う。
Poetryを使う場合は

  • pyproject.toml

を作る必要があるのでコンテナ作成時に
dockerfile内でスクリプトを実行して作成するようにした。
※pyproject.tomlがある場合は上書きはしない

また当記事内で使用するPoetryのコマンドは下記記事で解説している

なぜFastAPIでつくるのか?

REST APIをPythonで実装する場合、選択肢として有力なのは

  • Flask
  • FastApi

の二つになると思う。
※Djangoはフルスタックフレームワークなので少しオーバースペックのため除外

FastApiを選んだ理由

FastAPIを選択する利点は下記のようなものがある

パフォーマンス

FastAPIは非同期処理をネイティブにサポートしてるため
ASGI(Asynchronous Server Gateway Interface)サーバーとして
非同期対応の高速なサーバーであるUvicornで実行できる。

そのためパフォーマンスに優れている。
特にI/Oバウンドな処理(データベースやAPI呼び出しなど)においては
Flaskよりもパフォーマンスが高い。

Uvicornについては下記参照

自動生成されるドキュメント

FastAPIはエンドポイントに基づいてOpenAPI仕様に基づいたAPIドキュメントを自動生成してくれる。
開発者が手動でドキュメントを作成する必要がなく、APIの使い方を確認したり、テストしたりするのが非常に簡単になる

OpneAPIについては下記参照

入力バリデーションと型ヒント

Pythonの型ヒントに基づいた自動バリデーションが可能で、
Pydanticを使って入力データのバリデーションがシンプルに行える

このような利点があるためFaskApiを選択した。
ただ作るアプリケーションによってはケースバイケースだと思う

flaskで作る場合についての詳細は下記でまとめています

環境

下記の環境で行う

  • Windows10
  • Docker version 24.0.2(Docker for Windows)
  • VScode
  • Remote Development(VScodeの拡張機能)
  • fastapi 0.114.1
  • poetry 1.8.3

Docker for Windowsのインストール方法については下記記事で 紹介しています

構成

全体的なプロジェクト構成は下記のようにする

プロジェクト構成
.
|-- .vscode
| |-- launch.json
| `-- tasks.json
|-- app
| |-- __init__.py
| |-- asgi.py
| |-- endpoints.py
| `-- main.py
|-- docker-compose.yml
|-- dockerfile
|-- poetry.lock
|-- pyproject.toml
`-- script
|-- entrypoint.sh
|-- run_uvicorn.sh
`-- vscode_ex_install.sh

「pyproject.toml」と「poetry.lock」については
コンテナ作成時に「poetry init」コマンドを実行して作成するにで最初はなくていい。

コンテナ作成

VSCodeからdocker-composeを使ってコンテナを作成する

dockerfile

Dockerfile
# ベースイメージとしてPythonを使用
FROM python:3.12 as python-base
# 作業ディレクトリを作成
WORKDIR /workspace
# pipを更新
RUN pip install --upgrade pip
# Poetryをインストール
RUN pip install poetry
# スクリプトをコピー
COPY script/entrypoint.sh /workspace/script/
# スクリプトを実行するために権限を変更
RUN chmod +x /workspace/script/entrypoint.sh
# エントリーポイントとしてスクリプトを設定
ENTRYPOINT ["/workspace/script/entrypoint.sh"]

REST APIの実装に必要な依存関係をインストールしたDockerイメージを作成する

依存関係のインストールはPoetryを使って行う
エントリーポイントでPoetryコマンドのスクリプトを実行している

script/entrypoint.sh

entrypoint.sh
#!/bin/sh
# pyproject.toml が存在するかチェック
if [ ! -f pyproject.toml ]; then
echo "pyproject.toml が存在しないため、poetry init を実行します..."
# pyproject.toml が存在しない場合、poetry init を実行
poetry init -n --name fastapi-restapi --dependency fastapi --dependency uvicorn[standard]
echo "poetry init が完了しました。"
else
echo "pyproject.toml が既に存在します。poetry init は実行されませんでした。"
fi
# 依存関係をインストール
echo "依存関係をインストールしています..."
poetry install --no-root
echo "依存関係のインストールが完了しました。"
# # uvicorn サーバーを起動
# echo "仮想環境でuvicorn サーバーを起動しています..."
# sh /workspace/script/run_uvicorn.sh
tail コマンドでコンテナが終了しないようにする
tail -f /dev/null

Poetryコマンドの実行

スクリプトでPoetryコマンドを実行している
実行するコマンドは

  • poetry init (pyproject.tomlファイルを生成し、プロジェクトの初期化)
  • poetry install --no-root ( pyproject.tomlに基づいて依存関係をインストールする)

の二つ。

ただ「pyproject.toml」がある場合は「poetry init」する必要がないので
条件分岐を書いている

コメントアウトのuvicorn サーバーを起動について

コメントアウトを解除するとコンテナ作成と同時にuvicornサーバーで
REST APIが起動する。
デフォルトで起動すると止める時が面倒なので一旦は起動しないようにしておく。

tail -f /dev/nullについて

Dockerのコンテナには「プロセス単位で動作する」という特性がある
そのためコンテナの中で実行されているプロセスが終了すると、そのコンテナも終了してしまう。

そのため「tail -f /dev/null」コマンドを使うことで、コンテナが停止するのを防いでいる。

「uvicorn サーバーを起動」部分をコメントアウト解除した場合は
コンテナでプロセスが起動していることになるので
「tail -f /dev/null」部分は必要なくなる。

script/run_uvicorn.sh

run_uvicorn.sh
#!/bin/sh
poetry run uvicorn app.asgi:app --reload --host 0.0.0.0 --port 8000

poetryの仮想環境でuvicornを起動するコマンドをスクリプトにしたもの サーバーを起動する時にこのスクリプトを読み込んで起動する

コマンドを少し詳しく解説する

poetry run

仮想環境内で指定したコマンドを実行する

uvicorn app.asgi:app

FastApiアプリケーションインスタンスを指定してuvicornを起動
「app.asgi:app」はFastAPIアプリケーションインスタンスまでのパスを示してる
※app/asgi.py/app

--reload

uvicorn起動時のオプション
起動中にソースが変更された場合に再起動する。
※いわゆるホットリロード

--host 0.0.0.0

dockerのコンテナは、コンテナがそれぞれ独立した環境を持っているため
デフォルトではホストOSからアクセスできない状態になっている

そしてuvicornサーバーがデフォルトで待ち受けるIPアドレスが127.0.0.1(localhost)
のためIP、ポート番号なしで起動してもホストからはアクセスできない。

そのため起動時に

  • IPアドレスを0.0.0.0(すべてのIPアドレス)に設定

を指定して起動することでホストからアクセス可能になる。

--port 8000

ホストの8000番ポートとコンテナの8000番ポートをバインドさせるため
8000番で起動する

docker-compose.yml

docker-compose.yml
version: "3"
services:
fastapi:
container_name: "fastapi"
build:
context: .
dockerfile: Dockerfile
volumes:
- .:/workspace
tty: true
ports:
- 8000:8000

dockerfileからdockerイメージを作成してコンテナを作成する
ホストマシンのポート8000を、docker内のポート8000に接続する

docker-composeでコンテナ作成を実行

ここまでのファイルでVSCodeからコンテナを作成する
VSCodeのターミナルで下記コマンドを実行する

bash
# コンテナ作成実行
docker-compose up -d
# コンテナ作成確認
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
8e5ac4313646 faskapi_restapi-fastapi "/workspace/script/e…" 28 hours ago Up 9 seconds 0.0.0.0:8000->8000/tcp fastapi

コンテナが作成されていることがわかる。

pyproject.tomlについて

pyproject.tomlがない状態で実行した場合
プロジェクト直下に

  • pyproject.toml
  • poetry.lock

が作成される

コンテナのログを確認すると

コンテナログ
$ docker logs fastapi
pyproject.toml が存在しないため、poetry init を実行します...
Using version ^0.114.1 for fastapi
Using version ^0.30.6 for uvicorn
poetry init が完了しました。
依存関係をインストールしています...
Creating virtualenv fastapi-restapi-xS3fZVNL-py3.12 in /root/.cache/pypoetry/virtualenvs
Updating dependencies
Resolving dependencies... (6.5s)
Package operations: 18 installs, 0 updates, 0 removals
- Installing idna (3.8)
- Installing sniffio (1.3.1)
- Installing typing-extensions (4.12.2)
- Installing annotated-types (0.7.0)
- Installing anyio (4.4.0)
- Installing pydantic-core (2.23.3)
- Installing click (8.1.7)
- Installing h11 (0.14.0)
- Installing httptools (0.6.1)
- Installing pydantic (2.9.1)
- Installing python-dotenv (1.0.1)
- Installing pyyaml (6.0.2)
- Installing starlette (0.38.5)
- Installing uvloop (0.20.0)
- Installing watchfiles (0.24.0)
- Installing websockets (13.0.1)
- Installing fastapi (0.114.1)
- Installing uvicorn (0.30.6)
Writing lock file
依存関係のインストールが完了しました。
tail: cannot open 'コマンドでコンテナが終了しないようにする' for reading: No such file or directory

entrypoint.shが実行されていることがわかる。

作成されたpyproject.tomlは下記のようになっている

pyproject.toml
[tool.poetry]
name = "fastapi-restapi"
version = "0.1.0"
description = ""
authors = ["Your Name <you@example.com>"]
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.12"
fastapi = "^0.114.1"
uvicorn = {extras = ["standard"], version = "^0.30.6"}
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

依存関係としてfastapiとuvicornが設定されている
※entrypoint.shでpoetry init実行時のオプションで指定しているため

VSCodeからコンテナでのリモート開発方法

開発はVSCodeからリモートでコンテナに接続しコンテナ上で行う。

VSCodeの拡張機能である。

  • Dev Containers(ms-vscode-remote.remote-containers)
  • Docker(ms-azuretools.vscode-docker)

のどちらかを使用すれば、コンテナ内をVSCodeから操作できるので開発しやすい。
※この拡張機能はホスト側のVSCodeでインストールして使う

ちなみに自分はdocker-composeでコンテナを作った後
Docker(ms-azuretools.vscode-docker)でコンテナにアタッチしている
下記のような感じ
2024-09-15-17-07-25 別ウィンドウでコンテナにVSCodeからリモート接続することができる

Dev Containersの使い方については下記でまとめています。

コンテナに必要なVSCode拡張機能のインストール

リモートでつないだコンテナで下記を実行して必要な拡張機能を一括インストールする

bash
#!/bin/bash
# 拡張機能のIDリスト
extensions=(
"ms-python.python"
)
# 各拡張機能をインストール
for extension in "${extensions[@]}"; do
code --install-extension $extension
done

詳細は下記参照

REST APIの実装

簡単なREST APIを実装する。

REST API内で持っているdictに対してCRUDを行った
結果を返すREST APIを実装する

HTTPメソッドとしては

  • GET:参照
  • POST:登録
  • PUT:更新
  • DELETE:削除

のエンドポイントを定義したREST APIにする

FastAPIのデフォルトの処理について

実装していく前にFastAPIがデフォルトで行ってくれる処理についてまとめる
※pydanticを使って行える機能についてはここでは省く

ステータスコードが200になる

FastAPIでは、明示的に Responseオブジェクトやstatus_code を指定しない場合、
デフォルトでステータスコードは 200 OK になる。

test_endpoint
@router.get("/test")
async def test():
return {"message": "Success!"}

この場合、curlコマンドで確認すると

結果1
$ curl -i -X GET http://localhost:8000/test
HTTP/1.1 200 OK
date: Sat, 14 Sep 2024 07:58:24 GMT
server: uvicorn
content-length: 22
content-type: application/json
{"message":"Success!"}

200が設定されていることがわかる

また

test_endpoint
@router.get("/test", status_code=201)
async def test():
return {"message": "Success!"}

のようにすればデフォルトを変更できる

結果2
$ curl -i -X GET http://localhost:8000/test
HTTP/1.1 201 Created
date: Sat, 14 Sep 2024 07:59:56 GMT
server: uvicorn
content-length: 22
content-type: application/json
{"message":"Success!"}

201に変更できる

もしくは

test_endpoint
@router.get("/test", status_code=201)
async def test():
return Response(status_code=202, content=json.dumps({"message": "Success!"}), media_type="application/json")

Responseクラスを使えば自由に設定ができる。

結果3
$ curl -i -X GET http://localhost:8000/test
HTTP/1.1 202 Accepted
date: Sat, 14 Sep 2024 08:02:15 GMT
server: uvicorn
content-length: 23
content-type: application/json
{"message":"Success!"}

優先順位は

  • デフォルト < エンドポイントで設定 < Responseクラスで設定

の順になる

自動でJSON変換

FastAPIのエンドポイントが返すレスポンスデータを自動でJSONに変換してくれる

test_endpoint
@router.get("/test", status_code=201)
async def test():
users_dict = [
{"user_id": "1", "name": "Tujimura", "age": 11},
{"user_id": "2", "name": "mori", "age": 20},
]
return users_dict

ディクショナリをそのままレスポンスとして返却してもJSONに変換してくれる

結果4
$ curl -i -X GET http://localhost:8000/test
HTTP/1.1 201 Created
date: Sun, 15 Sep 2024 07:07:16 GMT
server: uvicorn
content-length: 83
content-type: application/json
[{"user_id":"1","name":"Tujimura","age":11},{"user_id":"2","name":"mori","age":20}]

リクエストデータ(パスパラメータ、クエリパラメータ)の自動パース

FastAPIでは型ヒントを使って自動パースをしてくれる

test_endpoint
@router.get("/test/{user_id}", status_code=201)
async def test(user_id: int):
users_dict = [
{"user_id": 1, "name": "Tujimura", "age": 11},
{"user_id": 2, "name": "mori", "age": 20},
]
user = next((user for user in users_dict if user['user_id'] == user_id), None)
return user
  • 「user_id」を型ヒントでintにする
  • 「user_id」を数値にしておく

パスパラメータで数値を指定した場合

結果5
$ curl -i -X GET http://localhost:8000/test/1
HTTP/1.1 201 Created
date: Sun, 15 Sep 2024 07:18:20 GMT
server: uvicorn
content-length: 40
content-type: application/json
{"user_id":1,"name":"Tujimura","age":11}

パスパラメータで指定した値を取得できる

パスパラメータで文字列を指定した場合

結果6
$ curl -i -X GET http://localhost:8000/test/abc
HTTP/1.1 422 Unprocessable Entity
date: Sun, 15 Sep 2024 07:19:23 GMT
server: uvicorn
content-length: 152
content-type: application/json
{"detail":[{"type":"int_parsing","loc":["path","user_id"],"msg":"Input should be a valid integer, unable to parse string as an integer","input":"abc"}]}

FastAPIは自動的にエラーを返す

クエリパラメータでも同様のことができる

test_endpoint
@router.get("/test", status_code=201)
async def test(age: int):
users_dict = [
{"user_id": 1, "name": "Tujimura", "age": 11},
{"user_id": 2, "name": "mori", "age": 20},
]
user = next((user for user in users_dict if user['age'] == age), None)
return user

クエリパラメータとしてageを受け取り検索する

クエリパラメータで数値を指定した場合

結果7
$ curl -i -X GET http://localhost:8000/test?age=20
HTTP/1.1 201 Created
date: Sun, 15 Sep 2024 07:32:53 GMT
server: uvicorn
content-length: 36
content-type: application/json
{"user_id":2,"name":"mori","age":20}

クエリパラメータで指定した値を取得できる

クエリパラメータで文字列を指定した場合

結果8
$ curl -i -X GET http://localhost:8000/test?age=abc
HTTP/1.1 422 Unprocessable Entity
date: Sun, 15 Sep 2024 07:33:09 GMT
server: uvicorn
content-length: 149
content-type: application/json
{"detail":[{"type":"int_parsing","loc":["query","age"],"msg":"Input should be a valid integer, unable to parse string as an integer","input":"abc"}]}

同様にFastAPIは自動的にエラーを返す

リクエストボディとレスポンスデータの検証について

リクエストボディとレスポンスデータの自動検証についてpydanticのデータモデルを使う必要がある。
厳密にいうとレスポンスデータの検証はデータモデルなしでも可能だが、データ検証する場合は
データモデルと併用するのが一般的であるためここでは割愛する。

pydanticは型ヒントを使用してデータのバリデーションを行うライブラリのことで
FastApiはPydanticをネイティブに統合している
※pydanticをインストールなしで使える

app/endpoints.py

エンドポイントを管理するモジュール
動作を確認するのが目的のため下記のような仕様にした

  • 各エンドポイントではディクショナリに対してCRUDを実施した結果を返す。
  • 結果を返すだけで実際のディクショナリは変更しない
app/endpoints.py
from fastapi import APIRouter, Request
from fastapi.responses import JSONResponse
import copy
router = APIRouter()
# ディクショナリ
users = [
{"user_id": "1", "name": "Tujimura", "age": 11},
{"user_id": "2", "name": "mori", "age": 20},
{"user_id": "3", "name": "shimada", "age": 50},
{"user_id": "4", "name": "kyogoku", "age": 70}
]
# デフォルト動作検証用のためコメントアウト
# @router.get("/test/{user_id}", response_model=List[dict])
# async def test(user_id: int):
# users_dict = [
# {"user_id": 1, "name": "Tujimura", "age": 11},
# {"user_id": 2, "name": "mori", "age": 20},
# ]
# user = next((user for user in users_dict if user['user_id'] == user_id), None)
# return user
# 参照
# ディクショナリの一覧を取得
@router.get("/")
async def get_users():
return JSONResponse(status_code=200, content=users)
# 条件指定参照
# ディクショナリの一覧からid指定で取得
@router.get("/{user_id}")
async def get_user_by_id(user_id: str):
user = next((user for user in users if user['user_id'] == user_id), None)
if user:
return JSONResponse(status_code=200, content= user)
return JSONResponse(status_code=404, content={"error": "User not found"})
# 登録
# ディクショナリに登録
@router.post("/")
async def post_user(request: Request):
user = await request.json()
res_users = copy.deepcopy(users)
res_users.append(user)
return JSONResponse(status_code=201, content=res_users)
# 更新
# ディクショナリをid指定で更新
@router.put("/{user_id}")
async def put_user(user_id: str, request: Request):
updated_user = await request.json()
res_users = copy.deepcopy(users)
for idx, user in enumerate(res_users):
if user['user_id'] == user_id:
res_users[idx] = updated_user
return JSONResponse(status_code=200, content=res_users)
return JSONResponse(status_code=404, content={"error": "User not found"})
# 削除
# ディクショナリからid指定で削除
@router.delete("/{user_id}")
async def delete_user(user_id: str):
res_users = list(filter(lambda user: user['user_id'] != user_id, users))
return JSONResponse(status_code=200, content=res_users)

エンドポイントとしては

  • GET:参照
  • POST:登録
  • PUT:更新
  • DELETE:削除

を定義し、HTTPメソッドごとに処理を行う。

上記コードの要点を解説する

FastAPIとasyncの関係

FastAPIはasync/awaitをサポートしており、エンドポイント関数を非同期にすることで、高いパフォーマンスを出せる。
そのためエンドポイントのメソッドには「async」をつけている。
※非同期が必要ない場合はasyncを外せば、同期処理をさせることができる

asyncありなしの処理の違いについて少し例を使ってまとめる。

asyncありの場合(非同期処理)

仮に一つのエンドポイントに対して10件のリクエストが来た場合

  • 1件のリクエストが処理されていて待ち時間が発生すると、次のリクエストを処理する。

非同期処理では、データベースアクセスや外部API呼び出しなどのI/O待ちの間、次のリクエストを処理することができる。 具体的には下記ようなイメージ

  1. リクエストAが送信される
  2. リクエストAでデータベース処理の待ち時間が発生
  3. リクエストBが処理される
  4. リクエストAのデータベース処理が完了したら、リクエストAの残りの処理を再開する。

待ち時間が発生するような処理の場合、その待ち時間を利用して他のリクエストを処理することで
複数のリクエストを効率的に処理することができるので非同期を使うとシステムのスループットが向上する

asyncなしの場合(同期処理)

仮に一つのエンドポイントに対して10件のリクエストが来た場合

  • 1件のリクエストが処理されている間、他のリクエストは待機する
  • たとえば、リクエストがデータベースアクセスや外部API呼び出しを含む場合、その処理が完了するまで次のリクエストは処理されない。

結果として、10件のリクエストは順番に1つずつ処理されることになる。 具体的には下記ようなイメージ

  1. リクエストAが送信される
  2. リクエストAのデータベース処理が完了するまで他のリクエストは処理されない
  3. リクエストBを処理する。

処理待ち時間が発生しないような単純な処理であればasyncをつけない方がシンプルで
パフォーマンスにも問題ない。
※今回書いたapp/endpoints.pyのようなコードでは待ち時間が発生する処理はないので実際は必要ない

APIRouterとは?

APIRouter は、FastAPIで複数のエンドポイント(ルート)をまとめて管理するためのクラス。

アプリケーションの初期化処理を行うモジュールとエンドポイントを管理するモジュールを
わけているためここで設定したエンドポイントはアプリケーションの初期化処理で
FastAPIアプリケーションインスタンスに設定する

JSONResponseクラスについて

JSON形式のレスポンスに特化したクラス。
FastAPIは、デフォルトでJSONレスポンスを返すが、「JSONResponse」を明示的に使うことで、

  • レスポンスの内容をJSON形式にエンコード、
  • 適切なヘッダー(Content-Type: application/json)

を自動的でしてくれる
特にカスタムステータスコードやヘッダーを設定したい場合に使う。
通常のAPI開発ではJSONResponseがよく使われる

例えばGETの処理の場合

GET処理抜粋
# 参照
# ディクショナリの一覧を取得
@router.get("/")
async def get_users():
return JSONResponse(status_code=200, content=users)

とした場合

  • ステータスコードを200
  • レスポンスデータをJSONに変換
  • media_typeをapplication/jsonに設定

をして返却してくれる

注意点として、JSONResponseは

  • 辞書形式(dict)
  • JSON 互換のデータ(リスト、基本的なデータ型など)

は自動的に JSON形式に変換するが、Pydanticモデルのオブジェクト(クラスのインスタンスなど)は
自動でJSON形式に変換してくれないので手動で対応する必要がある。

ちょっとサンプルを書くと

JSONResponse
from pydantic import BaseModel
# データモデル定義
class Users(BaseModel):
user_id: str
name: str
age: int
# 参照
# ディクショナリの一覧を取得
@router.get("/")
async def get_users():
# データモデルのインスタンス作成
invalid_user = Users(user_id="123", name="Add", age=30)
# インスタンスを戻り値に設定
return JSONResponse(status_code=200, content=invalid_user)

のようにデータモデルのインスタンスを直接、contentに設定しても
JSONResponseはJSONに変換してくれず
「TypeError: Object of type Users is not JSON serializable」が発生する

下記のように辞書形式に変換すればOK

JSONResponse
from pydantic import BaseModel
# データモデル定義
class Users(BaseModel):
user_id: str
name: str
age: int
# 参照
# ディクショナリの一覧を取得
@router.get("/")
async def get_users():
# データモデルのインスタンス作成
invalid_user = Users(user_id="123", name="Add", age=30)
# インスタンスを辞書形式に変換して戻り値に設定
return JSONResponse(status_code=200, content=invalid_user.model_dump())

app/main.py

アプリケーションの初期化処理を行うモジュール。
アプリケーションの初期化プロセスを関数にまとめる。
アプリケーションファクトリパターン(FlaskやFastAPIのプロジェクトで一般的に使われる設計パターン)で実装する

app/main.py
from fastapi import FastAPI
import app.endpoints as endpoints
def create_app():
# アプリケーションインスタンスの作成
app = FastAPI()
# ルーティング設定
app.include_router(endpoints.router)
return app

FastApiのアプリケーションインスタンスを生成して
エンドポイントを設定後、返却する関数を定義する。

アプリケーションファクトリパターンで実装することで
複数の設定や異なる構成に応じて、異なるアプリケーションインスタンスを作成することが容易になる

app/asgi.py

FastApiアプリケーションをASGIサーバーで起動するための エントリーポイントとなるモジュール

app/asgi.py
from app.main import create_app
app = create_app()

ファクトリ関数で生成したFaskApiアプリケーションインスタンスを取得し保持させる。

Uvicorn起動時にこのFastApiアプリケーションインスタンスまでのパスを指定して
起動することでUvicornでFaskApiアプリケーションを動かすことができる。

Uvicornを起動する際のスクリプトで下記のように指定している

script/run_uvicorn.sh

run_uvicorn.sh
#!/bin/sh
poetry run uvicorn app.asgi:app --reload --host 0.0.0.0 --port 8000

このスクリプトでは、uvicornコマンドで app/asgi.py 内の app というFastAPIアプリケーションインスタンスを指定して起動している。 poetry runは、Poetryが管理する仮想環境内で uvicornを実行するためのコマンド。
これにより、Poetryで管理されている依存関係を使って uvicornを実行する。

UvicornでFaskApiのREST APIを起動する

実装したREST APIをUvicornで起動する

起動はPoetryが管理する仮想環境内で行う
コマンドは

run_uvicorn
poetry run uvicorn app.asgi:app --reload --host 0.0.0.0 --port 8000

で起動できる

ターミナルで毎回コマンド入力したり、スクリプトを実行するのは面倒なので
VSCodeのタスクに登録しておく。手順は下記の通り

  1. 「F1」でコマンドパレットを開く
  2. 「テンプレートからtask.json」を生成
  3. 「Othes」を選択※後で書き換えるのでなんでもいい

上記を実行すると.vscode/task.jsonが作成されるので
下記ように書き換える

.vscode/task.json
{
"version": "2.0.0",
"tasks": [
{
"label": "Run Uvicorn with Poetry",
"type": "shell",
"command": "./script/run_uvicorn.sh", // 外部スクリプトを指定
"problemMatcher": [],
"isBackground": true,
"group": {
"kind": "build",
"isDefault": true
}
}
]
}

「script/run_uvicorn.sh」を読み込んで実行をタスクにしておく。

タスク登録後は「F1」でコマンドパレットを開き「タスク:タスクの実行」
を選択すると「Run Uvicorn with Poetry」というタスクが選択できるので
押すと実行できる
下記のようなイメージ 2024-09-15-22-20-02

実行がうまくできるとターミナルに下記が表示される

bash
* 実行するタスク: ./script/run_uvicorn.sh
INFO: Will watch for changes in these directories: ['/workspace']
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
INFO: Started reloader process [876] using WatchFiles
INFO: Started server process [919]
INFO: Waiting for application startup.
INFO: Application startup complete.

uvicornは、デフォルトで標準出力にログを出力する

リクエストを投げて確認する

ホストからcurlでリクエストを投げて確認する

GET①

bash
$ curl -i -X GET "http://localhost:8000/" -H "Content-Type: application/json"
HTTP/1.1 200 OK
date: Mon, 16 Sep 2024 07:17:59 GMT
server: uvicorn
content-length: 190
content-type: application/json
[{"user_id": "1", "name": "Tujimura", "age": 11}, {"user_id": "2", "name": "mori", "age": 20}, {"user_id": "3", "name": "shimada", "age": 50}, {"user_id": "4", "name": "kyogoku", "age": 70}]

一覧が取得できる

GET②

bash
$ curl -i -X GET "http://localhost:8000/1" -H "Content-Type: application/json"
HTTP/1.1 200 OK
date: Mon, 16 Sep 2024 07:18:20 GMT
server: uvicorn
content-length: 47
content-type: application/json
{"user_id": "1", "name": "Tujimura", "age": 11}

user_idで指定したデータを取得できている

POST

bash
$ curl -i -X POST "http://localhost:8000/" -H "Content-Type: application/json" -d '{"user_id": "5", "name": "yamada", "age": 30}'
HTTP/1.1 201 Created
date: Mon, 16 Sep 2024 07:18:38 GMT
server: uvicorn
content-length: 237
content-type: application/json
[{"user_id": "1", "name": "Tujimura", "age": 11}, {"user_id": "2", "name": "mori", "age": 20}, {"user_id": "3", "name": "shimada", "age": 50}, {"user_id": "4", "name": "kyogoku", "age": 70}, {"user_id": "5", "name": "yamada", "age": 30}]

登録したデータ込みの一覧を取得できている

PUT

bash
$ curl -i -X PUT "http://localhost:8000/1" -H "Content-Type: application/json" -d '{"user_id": "1", "name": "Tujimura", "age": 12}'
HTTP/1.1 200 OK
date: Mon, 16 Sep 2024 07:19:06 GMT
server: uvicorn
content-length: 190
content-type: application/json
[{"user_id": "1", "name": "Tujimura", "age": 12}, {"user_id": "2", "name": "mori", "age": 20}, {"user_id": "3", "name": "shimada", "age": 50}, {"user_id": "4", "name": "kyogoku", "age": 70}]

user_idが1のデータのageが更新されている

DELETE

bash
$ curl -i -X DELETE "http://localhost:8000/1" -H "Content-Type: application/json"
HTTP/1.1 200 OK
date: Mon, 16 Sep 2024 07:19:44 GMT
server: uvicorn
content-length: 141
content-type: application/json
[{"user_id": "2", "name": "mori", "age": 20}, {"user_id": "3", "name": "shimada", "age": 50}, {"user_id": "4", "name": "kyogoku", "age": 70}]

user_idで指定したデータを削除した一覧を取得できている

Uvicornでデバッグする

VSCodeからコンテナの仮想環境上のUvicorn上で起動しているREST APIをデバッグする

VSCodeでデバッグの設定をする

VScodeでデバッグをするための設定ファイル「launch.json」を作成する

.vscode/launch.json

.vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Python: FastAPI",
"type": "debugpy",
"request": "launch",
"module": "uvicorn",
"args": [
"app.asgi:app",
"--reload",
"--host",
"0.0.0.0",
"--port",
"8000"
],
"jinja": true,
"justMyCode": true
}
]
}

Pythonデバッガーは直接シェルスクリプトをサポートしていないため
スクリプト(script/run_uvicorn.sh)を読み込んで実行はできないので手動で設定する

またPoetryの仮想環境で動かすためインタープリンタに
Poetryの仮想環境を設定しておく

仮想環境の確認

ターミナルで確認

poetry仮想環境
fastapi-restapi-py3.12root@b87372f9e936:/workspace# poetry env list
fastapi-restapi-xS3fZVNL-py3.12 (Activated)

「fastapi-restapi-xS3fZVNL-py3.12」という仮想環境がActivatedになっている

インタープリンタの設定

VScodeのコマンドパレットで設定する
2024-09-16-16-29-57

仮想環境を指定 2024-09-16-16-31-32 「poetry env list」コマンドで確認した仮想環境を選択する

デバッグ実行

ブレークポイントを打ってデバッグする
デバッグの設定で8000番ポートを指定しているため
Uvicornの起動ポートと重複しているので、Uvicornの起動は停止しておく
下記ような感じでポート重複でエラーになる

ポート重複エラー
root@b87372f9e936:/workspace# cd /workspace ; /usr/bin/env /root/.cache/pypoetry/virtualenvs/fastapi-restapi-xS3fZVNL-py3.12/bin/python /root/.vscode-server/extensions/ms-python.debugpy-2024.10.0-linux-x64/bundled/libs/debugpy/adapter/../../debugpy/launcher 59589 -- -m uvicorn app.asgi:app --reload --host 0.0.0.0 --port 8000
INFO: Will watch for changes in these directories: ['/workspace']
ERROR: [Errno 98] Address already in use
root@b87372f9e936:/workspace#

またはデバッグの設定を8000番以外にする

ブレークポイントで止まる 2024-09-16-16-37-54

APIドキュメントを自動生成

FastAPIでは、アプリケーションを起動すると、OpenAPIドキュメントが自動生成され、
Webインターフェースで確認することができます。

Swagger UI で確認

ブラウザから「http://localhost:8000/docs」で Swagger UI形式のOpenAPIドキュメントのビジュアルインターフェースを確認できる。 2024-09-16-16-47-32

ReDoc で確認

ブラウザから「http://localhost:8000/redoc」で ReDoc形式のOpenAPIドキュメントのビジュアルインターフェースを確認できる。 2024-09-16-16-47-09

まとめ

VSCodeを使ってdockerコンテナ上でFastApiのREST APIを作ってみた。
今回は開発環境と動作環境に重きを置いたため、実際のREST API自体はシンプルになっている。

今まではpythonではflaskを使っていたが
非同期に対応できていて、かつドキュメントも自動生成できるFastApiも
かなり使いやすいと感じた。

新着記事

目次
タグ別一覧
top