🦓

FastAPIでCRUD APIエンドポイントを作成する

2023/12/10に公開

はじめに

FastAPIの学習でCRUD APIを作成してみました。
FastAPI は、Python 3.8+ で API を構築するためのWeb フレームワークです。

https://fastapi.tiangolo.com/

環境

Docker
Python 3.8+
MongoDB

tl;dr

  1. 仮想環境を作成する
  2. Dockerで環境構築する
  3. DBの設定を行う
  4. モデルを作成する
  5. エンドポイントを作成する
  6. CRUD操作を行う
  7. SwaggerUIを使って検証する

fastapiのディレクトリ構造

最低限の構造になりますが。

. # ルートディレクトリ
├── README.md
├── app
│   ├── __init__.py # 空で大丈夫。Pythonがファイルを含むディレクトリをパッケージとして扱うために必要
│   ├── main.py # スターターファイル
│   ├── __pycache__
│   ├── config 
│   ├── models
│   ├── routes
│   └── schemas
├── docker-compose.yaml
├── Dockerfile
├── requirements.txt # 必要なパッケージを記述する場所
├── .env
└── venv # 仮想環境

仮想環境を作成する

ルートディレクトリから作成します。

# pythonのバージョンを確認する
➜  python3 -V
Python 3.10.11

# venvを使って仮想環境を作成する
➜  python3 -m venv venv
# 作成されたことを確認する
➜  ls                  
venv

仮想環境を起動します。

# mac/linuxの場合
source venv/bin/activate
(venv)

仮想環境を中止する場合deactivateを打ちます。

https://docs.python.org/ja/3/library/venv.html

必要なパッケージを追加する

requirements.txtを作成し、こちらのパッケージを追加します。

requirements.txt
asgiref==3.4.1
click==8.0.1
dnspython==1.16.0
fastapi==0.68.1
h11==0.12.0
pydantic==1.8.2
pymongo==3.12.0
six==1.16.0
starlette==0.14.2
typing-extensions==3.10.0.0
uvicorn==0.15.0
パッケージ 用途
asgiref ASGI(Asynchronous Server Gateway Interface)のリファレンス実装
click コマンドラインツールの作成を支援するライブラリ
dnspython DNSプロトコルの実装を提供するライブラリ
fastapi 高性能なAPIを作成するためのWebフレームワーク
h11 HTTP/1.1のためのPythonライブラリ
pydantic データバリデーションのためのライブラリ
pymongo MongoDBデータベースとのやり取りのためのライブラリ
six Python 2と3の互換性を提供するためのライブラリ
starlette ASGIアプリケーションの構築を支援するライブラリ
typing-extensions 型ヒントのためのPython拡張ライブラリ
uvicorn ASGIアプリケーションを実行するためのサーバー

Dockerで環境構築する

Dockerfile
# ベースイメージ
FROM python:3.9.7

# カレントディレクトリを/appに設定する
# ここにrequirements.txtファイルとappディレクトリを置く
WORKDIR /app

# このファイルは頻繁に変更されるものではないので、Dockerはそれを検知してこのステップでキャッシュを使用し、次のステップでもキャッシュを有効にする
COPY requirements.txt requirements.txt

# パッケージをインストールする
RUN pip install --no-cache-dir --upgrade -r requirements.txt

# カレントディレクトリを/appディレクトリの中にコピーする。
# このディレクトリにはすべてのコードがあり、最も頻繁に変更されるものなので、コンテナ・イメージのビルド時間を最適化するために、これをDockerfileの最後の方に置く
COPY . .

# uvicornサーバーを起動する
# app.mainからappをインポートする
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]

no-cache-dirオプションは、ダウンロードしたパッケージをローカルに保存しないようにpipに指示します。これは、同じパッケージをインストールするためにpipを再度実行する場合のみですが、コンテナで作業する場合はそうではありません。

no-cache-dirはpipに関連するオプションで、Dockerやコンテナとは関係ないです。

upgradeオプションは、pipにパッケージをアップグレードするように指示します。
このステップでも、利用可能な場合はDockerキャッシュを使用します。

キャッシュを使用すると、開発中にイメージを何度もビルドする際に、毎回すべての依存関係をインストールすることなく、時間を節約できます。

docker-compose.yaml
version: '3'
services:
  fastapi:
    build: .
    volumes:
      - ./:/app
    ports:
      - 8000:8000  # ホストマシンのポート8000を、docker内のポート8000に接続する

コンテナのビルドと起動を行います。

(venv) ➜   docker-compose up --build -d
[+] Building 2.0s (10/10) FINISHED                                                             
 => [fastapi internal] load .dockerignore                                                 0.0s
 => => transferring context: 2B                                                           0.0s
 => [fastapi internal] load build definition from Dockerfile                              0.0s
 => => transferring dockerfile: 266B                                                      0.0s
 => [fastapi internal] load metadata for docker.io/library/python:3.9.7                   1.3s
 => [fastapi 1/5] FROM docker.io/library/python:3.9.7@sha256:8771691756bbf5beff80d64fca8  0.0s
 => [fastapi internal] load build context                                                 0.1s
 => => transferring context: 167.89kB                                                     0.1s
 => CACHED [fastapi 2/5] WORKDIR /app                                                     0.0s
 => CACHED [fastapi 3/5] COPY requirements.txt requirements.txt                           0.0s
 => CACHED [fastapi 4/5] RUN pip install --no-cache-dir --upgrade -r requirements.txt     0.0s
 => [fastapi 5/5] COPY . .                                                                0.3s
 => [fastapi] exporting to image                                                          0.2s
 => => exporting layers                                                                   0.2s
 => => writing image sha256:28782418da17882527ead598b19afdaa041fb8a271522d0f9e454abb087d  0.0s
 => => naming to docker.io/library/fastapi-tut-fastapi                                    0.0s
[+] Running 2/2
 ✔ Network fastapi-tut_default      Created                                               0.0s 
 ✔ Container fastapi-tut-fastapi-1  Started                                               0.5s 

https://fastapi.tiangolo.com/ja/deployment/docker/

DBの設定を行う

DBを作成する

cloud.mongodb.comへアクセスし、アカウントを作成します。
Build a DatabaseをクリックしDBを作成します。

Freeプランを選びます。
名前を入れてcreateをクリックします。

DBのユーザー名とパスワードを入れてCreate Userをクリックします。

Add my current IPAdressをクリックしてIPアドレスを取得します。

DBクラスタを作成されました。
ConnectをクリックしてDBに接続します。

https://www.mongodb.com/

DBを追加する

Access your data through toolsの下にいくつの接続手段がありますが自分の開発環境に合う手段で大丈夫です。
自分はCLIから接続しました。
接続しないでウェブページ上から確認することもできます。

Connect to your ApplicationのメニューをクリックしたらDBのURLが表示されます。
コピーします。

アプリ内に.envを作成し、DBのURLを追加します。
DBのパスワードを書き換える必要があるので忘れないでくださいー

.env
DB_URl=*************

環境変数を使うにはpython-dotenvというパッケージが必要なのでインストールします。

pip3 install python-dotenv
pip freeze > requirements.txt

config/database.py内にURLを追加する

config/database.py
# MongoDBクライエントをインポートする
from pymongo import MongoClient
# dotenvをインポートする
from dotenv import dotenv_values

config = dotenv_values(".env")

# DB URLを追加する
client = MongoClient(config.get("DB_URL"))

# DB名を追加する
db = client.get_database("fastapi")

# コレクションを作成する
collection_name = db.get_collection("todos")

コレクションは MongoDB ドキュメントのグループです。リレーショナルデータベースのテーブルに似てます。

モデルを作成する

名前、説明、完了かどうか、日付のフィールドを持つ Todo という Pydantic モデルを定義します。
PydanticモデルはFastAPIで一般的に使用され、入力されたリクエストデータを検証し、モデルの構造に基づいてAPIドキュメントを自動生成します。

app/models/todos_model.py
from datetime import datetime 
from pydantic import BaseModel

class Todo(BaseModel):
    name: str
    description: str
    completed: bool
    date: datetime

スキーマファイルを作成する

app/schemas/todos_schema.py
def todo_serializer(todo) -> dict:
    return {
        "id": str(todo["_id"]),
        "name": todo["name"],
        "description": todo["description"],
        "completed": todo["completed"],
        "date": todo["date"],
    }

def todos_serializer(todos) -> list:
    return [todo_serializer(todo) for todo in todos]

todo_serializertodos_serializer 2つの関数を定義しました。
これらの関数は MongoDB ドキュメント (todos) を簡単に JSON に変換できる形式にシリアライズする役割を担っています。

ObjectIdは、各ドキュメントの_idフィールドのデフォルト値で、ドキュメントの作成時に生成されます。

MongoDB の ObjectId は直接 JSON にシリアライズできないので、todo_serializer 関数の中で str(todo["_id"]) を使って "_id" フィールドを文字列に変換しています。

エンドポイントを作成する

TODOのCRUDオペレーションを行うAPIエンドポイントを追加します。

app/routes/todos_route.py
from fastapi import APIRouter

# モデル、DB、スキーマをインポートする
from app.models.todos_model import Todo
from app.config.database import collection_name
from app.schemas.todos_schema import todos_serializer, todo_serializer
from bson import ObjectId

# ルートを定義する
todo_api_router = APIRouter()

# GET
@todo_api_router.get("/")
async def get_todos():
    todos = todos_serializer(collection_name.find())
    return todos

@todo_api_router.get("/{id}")
async def get_todo(id: str):
    return todos_serializer(collection_name.find_one({"_id": ObjectId(id)}))


# POST
@todo_api_router.post("/")
async def create_todo(todo: Todo):
    _id = collection_name.insert_one(dict(todo))
    return todos_serializer(collection_name.find({"_id": _id.inserted_id}))


# UPDATE
@todo_api_router.put("/{id}")
async def update_todo(id: str, todo: Todo):
    collection_name.find_one_and_update({"_id": ObjectId(id)}, {
        "$set": dict(todo)
    })
    return todos_serializer(collection_name.find({"_id": ObjectId(id)}))

# DELETE
@todo_api_router.delete("/{id}")
async def delete_todo(id: str):
    collection_name.find_one_and_delete({"_id": ObjectId(id)})
    return {"status": "ok"}
  1. GET /:

    • @todo_api_router.get("/")は、TODOリストの全体を取得するためのエンドポイントです。
    • get_todos関数はcollection_name.find()を呼び出して、データベース内のすべてのTODOを取得します。
    • 取得したTODOはtodos_serializerを使用してシリアライズされ、JSON形式で返されます。
  2. GET /{id}:

    • @todo_api_router.get("/{id}")は、特定のIDに基づいてTODOを取得するためのエンドポイントです。
    • get_todo関数はcollection_name.find_one({"_id": ObjectId(id)})を呼び出して、指定されたIDのTODOを取得します。
    • 取得したTODOもtodos_serializerを使用してシリアライズされ、JSON形式で返されます。
  3. POST /:

    • @todo_api_router.post("/")は、新しいTODOを作成するためのエンドポイントです。
    • create_todo関数はcollection_name.insert_one(dict(todo))を呼び出して、新しいTODOをデータベースに挿入します。
    • 挿入されたTODOは_idを使用して再びデータベースから取得され、todos_serializerを使用してシリアライズされ、JSON形式で返されます。
  4. PUT /{id}:

    • @todo_api_router.put("/{id}")は、指定されたIDのTODOを更新するためのエンドポイントです。
    • update_todo関数はcollection_name.find_one_and_update({"_id": ObjectId(id)}, {"$set": dict(todo)})を呼び出して、指定されたIDのTODOを更新します。
    • 更新されたTODOは再びデータベースから取得され、todos_serializerを使用してシリアライズされ、JSON形式で返されます。
  5. DELETE /{id}:

    • @todo_api_router.delete("/{id}")は、指定されたIDのTODOを削除するためのエンドポイントです。
    • delete_todo関数はcollection_name.find_one_and_delete({"_id": ObjectId(id)})を呼び出して、指定されたIDのTODOを削除します。
    • 削除が成功した場合、{"status": "ok"}がJSON形式で返されます。

これらのエンドポイントを使用することで、TODOリストの作成、取得、更新、削除などの操作が可能になります。

MongoDBのCRUDメソッドについて公式ドキュメントをご参考ください。
https://www.mongodb.com/docs/v4.4/crud/

エンドポイントをインポートする

エンドポイントを作成したがmain.pyからアクセスできるようにインポートします。

app/main.py
from fastapi import FastAPI
from app.routes.todos_route import todo_api_router

app = FastAPI()
app.include_router(todo_api_router)

localhost:8000/docsへアクセスし、こちらの画面を表示されたらOKです。

SwaggerUIを使って検証する

Swagger UIは、OpenAPI Specification(以前はSwagger Specificationとしても知られていました)に基づいてAPIドキュメンテーションを提供するオープンソースのユーザーインターフェースです。
Swagger(またはOpenAPI)は、APIの記述、ドキュメンテーション、とクライアントとの相互作用を可能にするための標準の仕様です。

Swagger UIは、APIエンドポイント、操作、パラメータ、とレスポンスの情報を視覚的かつ対話的に表示できます。主に以下のような機能があります:

  1. APIの可視化: APIエンドポイントや操作の一覧を表示し、各エンドポイントの詳細情報に簡単にアクセスできます。

  2. リクエストのテスト: Swagger UIを使用してAPIエンドポイントにリクエストを送信し、レスポンスを確認できます。これにより、APIが期待どおりに動作するかを簡単にテストできます。

  3. 自動生成されたAPIドキュメンテーション: Swagger(またはOpenAPI)で提供されるAPI仕様を元に、自動的に生成されたAPIドキュメンテーションが表示されます。

  4. レスポンスの確認: 各APIエンドポイントがどのような種類のレスポンスを返すかを確認できます。これにはステータスコードやJSONスキーマなどが含まれます。

TODO一覧を取得する

Image from Gyazo

MongoDB側でCollectionsタブをクリックし、TODOを表示されていることも確認します。

TODOを作成する

Image from Gyazo

DB上に作成されたことも確認します。
nameを空にして作成するとエラーが出ることも確認します。

{
  "detail": [
    {
      "loc": [
        "body",
        12
      ],
      "msg": "Expecting value: line 2 column 11 (char 12)",
      "type": "value_error.jsondecode",
      "ctx": {
        "msg": "Expecting value",
        "doc": "{\n  \"name\": ,\n  \"description\": \"string\",\n  \"completed\": true,\n  \"date\": \"2023-12-09T16:01:21.701Z\"\n}",
        "pos": 12,
        "lineno": 2,
        "colno": 11
      }
    }
  ]
}

TODOを更新する

Image from Gyazo

DB上に更新されたことも確認します。

TODOを削除する

Image from Gyazo

DB上に削除されたことも確認します。

終わり

FastAPIでCRUD APIのエンドポイントの実装とDBとの連携を試してみました。
SwaggerUIのお陰で素早くエンドポイントを検証できて便利でしたー

Discussion