はじめに
本記事はFastAPIでバックエンドを開発する方法について記載しています。
FastAPIは、PythonでAPIを開発するためのモダンで高速(高性能)なWebフレームワークです。
Pydanticによる型ヒントを使用したデータの検証や、OpenAPIドキュメントを自動的に生成することができます。
また非同期プログラミングをサポートし、SQLAlchemyやTortoise-ORMと互換性、依存性の注入(DI)を考慮した実装も可能です。
Pythonを用いたマイクロサービスのバックエンド開発などには、最適なソリューションです。
FastAPIを使用する理由
なぜFastAPIを使用するかについて、FastAPIの主な特徴について以下に記載しています。
シンプル
後述するチュートリアルの通りに数行でAPIを開発することができます。
デフォルトの状態で適切に最適化されていますが、オプションを使用してパラメータを調整することもできます。
高速
Webフレームワークの性能評価の指標を算出しているWeb Framework Benchmarksより、2022-07-19 Round 21をご参照ください。(本記事執筆時点で最新のRound)
性能を基準に評価する際は、上記サイトは参考になると思います。
標準化
FastAPIは、OpenAPI Specificationを標準として、JSON Schemaと完全互換性があります。
OpenAPI Specification(以下、OAS)は、Linux Foundation共同プロジェクトであるOpenAPI Initiative内のコミュニティ主導のオープン仕様です。またOpenAPI自体がJSONスキーマに基づいています。
APIの標準およびプログラミング言語に依存しないインターフェイスによって、HTTPベースのコミュニケーションを簡素化します。
APIについて学ぶ
システム開発におけるソフトウェアの結合は、RPC、SOAP Webサービス、Web APIなどテクノロジーの進化によって、抽象化が進みました。
FastAPIもAPIを実現するための手段の一つですが、正しく使いこなすためには、Representational State Transfer(以下、REST)の概念や、API設計の考え方について理解していることが重要です。
本記事はFastAPIを題材にしているので深くは掘り下げませんが、APIの概念につて以下に記載しています。
APIとは
APIとは、アプリケーションプログラミングインタフェースの略称です。
APIには、以下のような種類があり、それぞれ用途が異なります。
- スマートフォンでカメラを起動して、写真を撮影するなどのハードウェアAPI
- Windows OS内部などで使用されるOS API
- 外部に公開していない内部利用を目的とした、インターナルAPI
- 外部から利用可能なパブリックAPI(Open APIとも呼ばれ、YAMLまたはJSONで記述)
APIを設計するということは、利用するユーザーとコンポーネント間のインターフェースを定義するということです。APIはシステムにおける内部の実装を抽象化したものすぎません。
現実世界で例えると、ATMでお金を下ろす場合、どのような仕組みで動いているかについて気にする人は少ないと思います。
興味があるのは、どこにカードを入れて、どのような操作をしたらお金を預けたり、引きだしができるなど、インターフェースの仕様ではないでしょうか。
API設計の重要性
API設計がなぜ重要なのか考えてみましょう。
例えば、APIが適当に設計されている場合は、以下のような問題が発生する可能性があります。
- APIが正しく使わなれないため、十分に活用されない
- APIが分かりにくいことによるユーザーからの問い合わせや、クレームが増えることによる運用コストの増加
- 設計の欠陥によって、データ漏洩などセキュリティに関わる脆弱性に繋がる
API設計は、原理原則に基づいたテクノロジーに対する理解や、ユーザー側からの視点など多岐にわたります。
APIは誰のためのものか
APIは目標を達成するための手段として、ユーザーが欲しいものを提供することが求められます。
例えば、リモコンはシンプルです。
テレビだろうがオーディオだろうが、大体は手に取ることができるサイズで、同じようなボタンの配置をしていて、何を目的としているかがはっきりと分かります。
API設計では、APIを使用するユーザーから見て、以下に示すような配慮が必要不可欠です。
- そのAPIで何ができて、どのようにして使用するのか
- APIをリクエストするための方法
- APIが返すレスポンスの期待値、例外発生時のエラーメッセージ
REST API
REST API(またはRESTful API)は、RESTアーキテクチャスタイルの制約に従って策定されたAPIのことです。
2000年にRoy Fieldingによって発表された、以下論文によってその設計思想が広まりました。
RESTfulの原則は以下の通りです。
- クライアント、サーバー、リソースからなるクライアント/サーバーアーキテクチャで、要求はHTTP経由で処理される
- ステートレスな通信は各要求を独立し、分離する
- クライアントは、サーバーのレスポンスをキャッシュすることができる
-
統一インターフェースは以下の要件を満たすこと
- 要求されるリソースは識別可能
- クライアントは表現を用いてリソースを操作できる
- 自己記述的メッセージによって、クライアントが情報をどのように処理すべきかを記述する十分な情報が含まれている
- HATEOAS
- 要求された情報は階層化システムである
- コードオンデマンドはサーバーからクライアントに実行可能コードを送信することで、クライアントは機能を拡張できる
OpenAPI Specification
OASは、REST API記述フォーマットです。
以前はSwagger Specificationと呼ばれていたもので、2015年11月にOAIに寄贈され、2016年1月に現在の運用になっています。OpenAPIの詳細については、What Is OpenAPI?をご参照ください。
OASドキュメントはテキストファイルで、リソース、アクション、パラメータ、レスポンスを記述します。
Swagger Editorを使用して編集することができます。
OASを使用することの最大のメリットは、単純に開発を効率よく進めることができるの一言に尽きると思います。
例えば、フロントエンドはバックエンド側の実装を待たなくても、OASを使用してモックサーバを起動してフロントエンドの開発スピードを上げることができます。
OAS自体がドキュメントになるので、エクセルなどによるドキュメント管理が不要です。
またOASに準じて宣言的に書くことは、曖昧な記述がなくなるため、仕様が明確になります。
FastAPI開発
APIの概念がある程度理解できた段階で、FastAPIの開発を行ないましょう。
前提として、本記事の環境は以下の通りです。
項目 | バージョン等 |
---|---|
OS | macOS |
Python | 3.9.4 |
fastapi | 0.88.0 |
Docker | 20.10.6 |
チュートリアル
チュートリアルの最初のステップでは、FastAPIの基本的な機能について学ぶことができます。
早速、チュートリアルを行うために、Pythonの仮想環境で試してみましょう。
Pythonの仮想環境はvenvを使用して作成します。
まずは仮想環境を作成するために、ターミナルなどを起動して、以下のコマンドを実行します。
<Dir>
には任意のディレクトリ名を指定します。
$ python3 -m venv <Dir>
$ source <Dir>/bin/activate
仮想環境構築後、以下のコマンドを実行してすべてのオプションの依存関係と機能をインストールします。
$ pip3 install "fastapi[all]"
pipでインストール完了後、以下のコマンドを実行すると、必要なパッケージについてインストールされていることが確認できます。
$ pip3 list
出力例
Package Version
----------------- ---------
anyio 3.6.2
certifi 2022.12.7
click 8.1.3
dnspython 2.2.1
email-validator 1.3.0
fastapi 0.88.0
h11 0.14.0
httpcore 0.16.3
httptools 0.5.0
httpx 0.23.1
idna 3.4
itsdangerous 2.1.2
Jinja2 3.1.2
MarkupSafe 2.1.1
orjson 3.8.3
pip 20.2.3
pydantic 1.10.2
python-dotenv 0.21.0
python-multipart 0.0.5
PyYAML 6.0
rfc3986 1.5.0
setuptools 49.2.1
six 1.16.0
sniffio 1.3.0
starlette 0.22.0
typing-extensions 4.4.0
ujson 5.6.0
uvicorn 0.20.0
uvloop 0.17.0
watchfiles 0.18.1
websockets 10.4
次にmain.py
を作成した後に、以下のコマンドを実行してuvicorn
で起動します。
- main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
return {"message": "Hello World"}
$ vi main.py
$ uvicorn main:app --reload
INFO: Will watch for changes in these directories: ['/Users/python/Desktop']
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO: Started reloader process [72817] using WatchFiles
INFO: Started server process [72819]
INFO: Waiting for application startup.
INFO: Application startup complete.
上記のような出力を確認後、Chrome ブラウザを起動します。
http://127.0.0.1:8000/
にアクセスすると、{"message":"Hello World"}
のJSON形式のメッセージが確認できます。
またデベロッパー ツールを使用すると、レスポンスヘッダからuvicornなどが確認できます。
http://127.0.0.1:8000/docs
にアクセスすると、自動生成されたSwagger UIで提供されているインタラクティブなドキュメントが確認できます。
ここまで5分もかからないと思いますが、たったの数行でAPIの基礎となる土台を作ることができました。
引き続き、チュートリアルのユーザーガイドを参照することで、基本的なAPI開発の実装に関して学ぶことができます。
Dockerを使用したAPI開発
Dockerを使用してAPI開発を行う例について以下に記載します。
ソースをマウントしている状態になるため、コードの変更は自動的に行われることで、柔軟に開発を行うことができます。
以下ではAPIの動きを理解するために、ルールティングの設定と、セキュリティ対策としてAPIキーでバックエンドを保護する実装を行っています。
環境構築
ソースを格納しているカレントディレクトリ(.)を起点にした、本記事のサンプルコードは以下の構成です。
.
├── backend
│ ├── Dockerfile
│ └── api
│ ├── main.py
│ ├── routers
│ │ └── test.py
│ └── util
│ └── util.py
└── docker-compose.yaml
- docker-compose.yaml
version: '3'
services:
demo-api:
build:
context: "./backend"
dockerfile: "Dockerfile"
ports:
- 8000:8000
volumes:
- ./backend/api:/src
environment:
- ENVIRON
- Dockerfile
FROM python:3.9-buster
ENV PYTHONUNBUFFERED=1
WORKDIR /src
COPY api ./
RUN pip3 install --upgrade pip
RUN pip3 install fastapi
RUN pip3 install "uvicorn[standard]"
RUN pip3 install requests
ENTRYPOINT ["uvicorn", "main:app", "--host", "0.0.0.0", "--reload"]
FROMで指定しているpython:3.9-buster
は、Debian 10を基にしたイメージです。
またdockerhubに格納されているpythonのイメージはOfficial Imagesのpythonから確認できます。名前にslim
が付いているイメージは軽量です。
- main.py
from fastapi import Depends, FastAPI, HTTPException, Security
from fastapi.security.api_key import APIKeyHeader, APIKey
from starlette.status import HTTP_403_FORBIDDEN
from routers import test
from util import util
correct_key: str = util.get_apikey()
api_key_header = APIKeyHeader(name='Authorization', auto_error=False)
async def get_api_key(
api_key_header: str = Security(api_key_header),
):
if api_key_header == correct_key:
return api_key_header
else:
raise HTTPException(
status_code=HTTP_403_FORBIDDEN, detail="Could not validate credentials"
)
app = FastAPI()
app.include_router(test.router, dependencies=[Depends(get_api_key)], tags=["Test"])
@app.get("/")
def read_root():
return {"status": "ok"}
- test.py
import os
from typing import Dict, List, TypedDict
from fastapi import APIRouter, Depends, HTTPException
ENVIRON = os.environ['ENVIRON']
router = APIRouter()
@router.get("/v1/list/fruit")
async def get_list_fruit() -> Dict:
if ENVIRON == 'dev':
response = {
'name': 'apple',
'price': '100',
}
elif ENVIRON == 'prd':
response = {
'name': 'orange',
'price': '100',
}
return response
- util.py
def get_apikey() -> str:
response = "dummy"
return response
本記事ではサンプルになるので便宜上、上記のような表現にしています。
実際に実装する場合、APIキーの値などはハードコーディングせずに、シークレット管理システムなどから取得するようにしましょう。
上記コードを配備後、以下のコマンドを実行してデプロイします。
$ export ENVIRON='dev'
$ docker-compose up -d
出力例
Docker Compose is now in the Docker CLI, try `docker compose up`
Building demo-api
[+] Building 40.3s (13/13) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 306B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/python:3.9-buster 2.5s
=> [auth] library/python:pull token for registry-1.docker.io 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 3.81kB 0.0s
=> [1/7] FROM docker.io/library/python:3.9-buster@sha256:5e28891402c02291f65c6652a8abddedcb5af15933e923c07c2670f836243833 23.9s
=> => resolve docker.io/library/python:3.9-buster@sha256:5e28891402c02291f65c6652a8abddedcb5af15933e923c07c2670f836243833 0.0s
=> => sha256:5e28891402c02291f65c6652a8abddedcb5af15933e923c07c2670f836243833 988B / 988B 0.0s
=> => sha256:c7c50787e2da71205402343dd1233b3ca6ebe70c7e790f6ba7d856b81b893200 50.45MB / 50.45MB 2.7s
=> => sha256:821dc261e045ba09851c52ed28be3b9ecc9fe8c923fba05854ab0fd2fa38ceca 10.00MB / 10.00MB 1.9s
=> => sha256:a21bc1f5190fa4f5d13dbdec6adc8d65ae2d3025411ecab367f616831dd570cb 2.22kB / 2.22kB 0.0s
=> => sha256:509942a964375108ec41c5663ed58a9d5f52f49336a113f29688ce599b9b0bee 8.51kB / 8.51kB 0.0s
=> => sha256:aff359114acb7d843de09375f87669fffb0abd1125c16f96431bc3c4173b48f8 7.86MB / 7.86MB 1.4s
=> => sha256:3790459d63d5fdf148a092f1857010796dfa217329491b66a015a40de92f3db9 51.87MB / 51.87MB 4.8s
=> => sha256:c09b016fff62bb8002f99d7e639c0a2c0ce5a774796aae1e446e99deeb1ac01d 191.89MB / 191.89MB 10.5s
=> => sha256:6aa81cf5366ed8cc72bf3e9865b2a33a023c602226b7c83b584b33fa1c443246 6.15MB / 6.15MB 3.4s
=> => extracting sha256:c7c50787e2da71205402343dd1233b3ca6ebe70c7e790f6ba7d856b81b893200 3.6s
=> => sha256:4c2f5a834e3b5c8b7a0195ef5d22bc8480e9879cd4c38b93dcd0df9bbe559ad6 19.92MB / 19.92MB 5.0s
=> => sha256:1d79b8cadecb9bf336caef07c858791fef8942063d9da716cee84e03aa9f5e00 233B / 233B 5.1s
=> => sha256:146e10e712ef0ac2ba6baf3895242f97e3dd52e72f02bbdbbb21700f8362391c 2.88MB / 2.88MB 5.7s
=> => extracting sha256:aff359114acb7d843de09375f87669fffb0abd1125c16f96431bc3c4173b48f8 0.6s
=> => extracting sha256:821dc261e045ba09851c52ed28be3b9ecc9fe8c923fba05854ab0fd2fa38ceca 0.5s
=> => extracting sha256:3790459d63d5fdf148a092f1857010796dfa217329491b66a015a40de92f3db9 3.3s
=> => extracting sha256:c09b016fff62bb8002f99d7e639c0a2c0ce5a774796aae1e446e99deeb1ac01d 9.3s
=> => extracting sha256:6aa81cf5366ed8cc72bf3e9865b2a33a023c602226b7c83b584b33fa1c443246 0.4s
=> => extracting sha256:4c2f5a834e3b5c8b7a0195ef5d22bc8480e9879cd4c38b93dcd0df9bbe559ad6 0.9s
=> => extracting sha256:1d79b8cadecb9bf336caef07c858791fef8942063d9da716cee84e03aa9f5e00 0.0s
=> => extracting sha256:146e10e712ef0ac2ba6baf3895242f97e3dd52e72f02bbdbbb21700f8362391c 0.3s
=> [2/7] WORKDIR /src 0.3s
=> [3/7] COPY api ./ 0.1s
=> [4/7] RUN pip3 install --upgrade pip 4.7s
=> [5/7] RUN pip3 install fastapi 3.6s
=> [6/7] RUN pip3 install "uvicorn[standard]" 2.9s
=> [7/7] RUN pip3 install requests 1.8s
=> exporting to image 0.4s
=> => exporting layers 0.4s
=> => writing image sha256:5f62e853a8f8569a6c75bc7b4a59f9912e855a890022b3c2e9d647808d840626 0.0s
=> => naming to docker.io/library/sample_demo-api 0.0s
WARNING: Image for service demo-api was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
Creating sample_demo-api_1 ... done
以下のコマンドを実行し、バックエンドのコンテナが正常に起動できていることを確認します。
$ docker logs -f sample_demo-api_1
INFO: Will watch for changes in these directories: ['/src']
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
INFO: Started reloader process [1] using WatchFiles
INFO: Started server process [8]
INFO: Waiting for application startup.
INFO: Application startup complete.
ルーティング確認
デプロイ完了後、Chrome ブラウザからhttp://localhost:8000/
にアクセスします。
レスポンスのメッセージ及びStatus Code
は200 OK
が返ってきていることが確認できます。
次にルーティング設定している、http://localhost:8000/v1/list/fruit
にアクセスします。
レスポンスのメッセージ及びStatus Code
は403 Forbidden
が返ってきていることが確認できます。
APIキーで保護しているため、このままではアクセスすることはできません。
HTTPリクエストのヘッダに、APIキーを付与してリクエストを送る必要があります。
app.include_router()
関数を使用することで、APIのエンドポイントが追加できることが分かります。
ルーティングの仕様など、他のWebフレームワークからインスピレーションを受けていることが代替ツールから受けたインスピレーションと比較に記載されています。
APIキーによるバックエンドの保護
http://127.0.0.1:8000/docs
にアクセスし、Swaggerの画面からPIキーを設定してみましょう。
認証を行うために、下三角の箇所を押します。
「Try it out」を押します。
「Excute」を押すことで、リクエストを送ることができますが、この状態では先ほど同じ結果になるので、右上の鍵マークのアイコンを押します。
「Available authorizations」の画面が表示されるので、「Value」にはAPIキーの値を入力して、「Authorize」を押します。
「Close」を押します。
再度、「Excute」を押します。
今度は、APIのレスポンスが確認できます。
APIキーを保護する仕組みを実現しているのは、dependencies=[Depends(get_api_key)]
による箇所です。
依存性の注入の詳細については、Dependenciesをご参照ください。
環境変数を変えてデプロイする
docker-compose.yaml
のenvironment
で環境変数を設定しています。
環境変数を変えてデプロイしてみましょう。
以下のコマンドを実行して、一度コンテナを削除します。
$ docker rmi -f sample_demo-api
Untagged: sample_demo-api:latest
環境変数ENVIRON
をprd
に設定してデプロイします。
$ export ENVIRON='prd'
$ docker-compose up -d
dockerの仕組みとしてCACHED
と表示されている通りに、2回目以降はキャッシュを利用してビルドしていることが確認できます。
出力例
Docker Compose is now in the Docker CLI, try `docker compose up`
Building demo-api
[+] Building 2.1s (13/13) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 37B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/python:3.9-buster 1.8s
=> [auth] library/python:pull token for registry-1.docker.io 0.0s
=> [1/7] FROM docker.io/library/python:3.9-buster@sha256:5e28891402c02291f65c6652a8abddedcb5af15933e923c07c2670f836243833 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 485B 0.0s
=> CACHED [2/7] WORKDIR /src 0.0s
=> CACHED [3/7] COPY api ./ 0.0s
=> CACHED [4/7] RUN pip3 install --upgrade pip 0.0s
=> CACHED [5/7] RUN pip3 install fastapi 0.0s
=> CACHED [6/7] RUN pip3 install "uvicorn[standard]" 0.0s
=> CACHED [7/7] RUN pip3 install requests 0.0s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:5f62e853a8f8569a6c75bc7b4a59f9912e855a890022b3c2e9d647808d840626 0.0s
=> => naming to docker.io/library/sample_demo-api 0.0s
今回はENVIRON
がprd
になるため、responseは先ほどとは別の結果が返ってきているのが確認できます。
ビルド時のキャッシュを削除する場合は、docker builder prune
コマンドを実行します。
なお、ビルド時にキャッシュを利用しないようにするためには、docker-compose build --no-cache
を実行します。
開発に役立つツール
PostmanはAPI開発の必需品です。
いくつかの機能がありますが、基本機能であるCollections機能は無料で利用できるので個人開発に重宝します。
複数人のチームでシナリオを共有する場合などは、有料プランの検討が必要になってくると思います。
Postmanの公式サイトからデスクトップアプリがダウンロードできます。
- 実行例
おわりに
歴史、設計、そしてこれからの通りに、FastAPIの歴史はまだ旅路の途中です。
2018年12月にリリースされているので他のWebフレームワークと比べると、まだ歴史が浅く、不足している機能もあると思いますが、Netflixなどでも使用されています。
Stack Overflow 2021 Developer Surveyでは、3番目に人気のあるWebフレームワークとなっています。