- 記事一覧 >
- ブログ記事
Python FastAPI React PostgreSQLのプロジェクトを動かして紐解いてみた
はじめに
バックエンド:Python FastAPI
フロントエンド:TypeScript React
DB:PostgreSQL
の組み合わせで簡単に開発を始められるもの(スターター、プロジェクトジェネレーター)を GitHub で見つけました。
↓https://github.com/Buuntu/fastapi-react
FastAPI 公式のものは、https://github.com/tiangolo/full-stack-fastapi-postgresql
で、フロントエンドが Vue です。これの React 版のようです。
...が、しかし、しばらく更新されていないせいなのか、TypeScript error が発生したのと、少々大げさに見えたので、別のものを見つけました。
↓https://github.com/gaganpreet/fastapi-starter
今回、これを動かしてみましたので、動かし方、動作内容を勝手に紹介していきたいと思います。
このプロジェクトの全体像は、以下です。Docker で起動します。
【 FastAPI 】
FastAPI は、Pythonの標準である型ヒントに基づいてPython 3.6 以降でAPI を構築するための、モダンで、高速(高パフォーマンス)な、Web フレームワークです。
【 React 】
React は、Meta社とコミュニティによって開発されているユーザインタフェース構築のためのJavaScriptライブラリです。コンポーネントと呼ばれる部品(UI・画面の一部分)ごとに開発ができます。画面更新の際、差分を検出して差分だけ更新するため、動作が速いという特徴があります。
【 PostgreSQL 】
拡張性とSQL準拠を強調するフリーでオープンソースの関係データベース管理システムです。
環境構築は、前回記事「Alpine Linux をインストールして VS Code Remote SSH してみた」の環境の続きからとします。OS は、Alpine Linux で、Python3 と pip3 がインストール済みです。
【検証環境】
Windows10 PRO x64
VS Code 1.68.1
VMware Workstation 16 Pro
alpine-virt-3.16.0-x86_64
Python 3.10.5
Docker インストール
docker-compose up
を使うため、Docker のインストールが必要です。
前回記事に書きましたので、実施済み前提で進めますが、
/etc/apk/repositories
の変更が必要です。
# apk update
# apk add docker docker-compose
# docker -v
Docker version 20.10.17, build 100c70180fde3601def79a59cc3e996aa553c9b9
# docker-compose -v
docker-compose version 1.29.2, build unknown
# /etc/init.d/docker start
OS を再起動しても Docker デーモンが起動するように自動起動サービスに追加します。
# rc-update add docker
プロジェクト構築
cookiecutter を使ってプロジェクトを作成します。
【 cookiecutter 】
プロジェクトテンプレートからプロジェクトを作成するコマンドラインユーティリティです。(https://github.com/gaganpreet/fastapi-starter にあるのは、プロジェクトテンプレート。)
# pip3 install cookiecutter
# cd /home/admin/
# cookiecutter https://github.com/gaganpreet/fastapi-starter
project_name [Sample project]:(エンター)
project_slug [sample-project]:(エンター)
backend_port [8000]:(エンター)
frontend_port [3000]:(エンター)
# cd sample-project
# cat .env
# POSTGRES_DB and POSTGRES_PASSWORD are used by the postgres docker image to initialse the db
POSTGRES_PASSWORD=WhhKGoAfTQpIAFbULLQIEwHqdkDAdrlG
POSTGRES_DB=app
DATABASE_URL=postgresql://postgres:WhhKGoAfTQpIAFbULLQIEwHqdkDAdrlG@postgres/app
TEST_DATABASE_URL=postgresql://postgres:WhhKGoAfTQpIAFbULLQIEwHqdkDAdrlG@postgres/apptest
SECRET_KEY=KhZFiewUoaBtMsfOyhdPGxfcjjPjkFOq
BACKEND_CORS_ORIGINS='["http://localhost:3000","http://127.0.0.1:3000"]'
PostgreSQL のパスワードが自動生成されます。
# docker-compose up -d
DB 作成 - migrate
docker-compose up
だけかと思っていましたが、自力で DB 作成&マイグレーションが必要です。
よく見ると、出来上がったプロジェクトの README.md に書いてありました。
ちなみにですが、https://github.com/Buuntu/fastapi-react
の方は、用意されている ./scripts/build.sh
で docker-compose up
と DB マイグレーションと初期ユーザー登録が完了して、動作確認開始できました。
DB の app については、docker-compose.yml
に以下のように書いてあるため、環境変数:POSTGRES_DB=app
が反映されて、 docker-compose up
により、自動的に作成されます。
env_file:
- .env
# docker-compose exec backend alembic upgrade head
INFO [alembic.runtime.migration] Context impl PostgresqlImpl.
INFO [alembic.runtime.migration] Will assume transactional DDL.
INFO [alembic.runtime.migration] Running upgrade -> 7e09fa75df7a, Add initial migration
INFO [alembic.runtime.migration] Running upgrade 7e09fa75df7a -> 14bc3b4ecc3a, Add item migration
# docker-compose exec postgres createdb apptest -U postgres
docker-compose exec [docker-compose.yml に書かれたサービス名] [コマンド]
により、コンテナの中に入ってマイグレーションしています。
自動テスト用の apptest DB は、自動的に作成されないため、自力作成です。
【 alembic 】
Alembic(アランビック)=蒸留器
Python で SQLAlchemy を使用しているときに DB の管理をしてくれる migration ツールです。
マイグレーションとは、DBに保存されているデータを保持したまま、テーブルの作成やカラムの変更などを行うための機能です。
マイグレーション機能を使うことで特定のデータベースに依存せずにデータベースの操作ができます。
【 SQLAlchemy 】
Alchemy(アルケミィ)=錬金術
Pythonで利用されるORMの1つです。
SQLAlchemyではコマンドでの操作やAlter Tableなどは行えませんが、Alembicではコマンドを用いたスキーマの操作やAlter Tableを行うことができます。
【O/Rマッピング(ORM)】
O/Rマッピングとは、オブジェクトとリレーショナルデータベース(RDB)の間でデータ形式の相互変換を行うことです。
Pythonオブジェクトとリレーショナルデータベース上のデータを関連付け、SQLを書かなくても、データベース操作ができる仕組みです。
frontend アクセス
frontend の http://localhost:3000/
にアクセスします。...としたかったですが、検証環境の OS が Alpine Linux で GUI が無いため、別のPCから http://192.168.11.9:3000/
でアクセスしています。
http://192.168.11.9:3000/register
画面でユーザー登録しようとすると、 https://192.168.11.9/api/v1/auth/register
へアクセスが行って、エラーになりました。
frontend エラー対処
http://192.168.11.9:8000/api/v1/auth/register
へアクセスしなくてはいけないと思うのですが、https://
へ行ってしまいます。
frontend/src/providers/env.ts
の実装を見ると、NODE_ENV=production かつ、localhost
以外の時は、https://[元々のホスト名]
が API だと思ってしまい、避けられませんでした。
(frontend/Dockerfile 中で yarn build
しているため、NODE_ENV=production
になり、NODE_ENV は、どうやっても上書きできませんでした。)
const readApiBaseFromEnv = (): string => {
// Get API base URL from env
// Priority is given to same host in the browser when environemnt is production
if (
process.env.NODE_ENV === "production" &&
!document.location.host.startsWith("localhost")
) {
return `https://${document.location.host}`;
} else if (process.env.REACT_APP_API_BASE) {
return process.env.REACT_APP_API_BASE;
}
return "http://localhost:8000";
};
今回あくまでもお試しなので、以下のように書き換えました。(return `https://${document.location.host}`
を書き換え。)
const readApiBaseFromEnv = (): string => {
// Get API base URL from env
// Priority is given to same host in the browser when environemnt is production
if (
process.env.NODE_ENV === "production" &&
!document.location.host.startsWith("localhost")
) {
//return `https://${document.location.host}`;
return `http://192.168.11.9:8000`;
} else if (process.env.REACT_APP_API_BASE) {
return process.env.REACT_APP_API_BASE;
}
return "http://localhost:8000";
};
frontend/.env.production
にREACT_APP_API_BASE=http://192.168.11.9:8000
と書くと、process.env.REACT_APP_API_BASE
がセットされますが、
今回の環境の場合、一番上の if で入ってきてしまうため、この対処になります。
# docker-compose up -d --build frontend
もう一度、http://192.168.11.9:3000/register
でユーザー登録をトライします。
ちゃんと API の http://192.168.11.9:8000/api/v1/auth/register
へ行っていますが、またエラーです。
CORS エラー対処
エラー内容は、CORS エラーです。ポート番号違いでオリジンが異なるため、ブラウザ側エラー判断です。CORS エラーがどういったものなのかについての詳細は、別記事に詳しく書いています。「OpenResty(Nginx)Lua で CORS 用に条件付きでレスポンスヘッダーを書き換え」
本当は、以下の構成にしたら良いような気がしますが、そうなっていません。(Buuntu/fastapi-react
の方は、nginx.conf
の proxy_pass
で、そうなっていました。)
このまま CORS エラーを解消します。
.env
にBACKEND_CORS_ORIGINS='["http://localhost:3000","http://127.0.0.1:3000"]'
というのがあり、ここにフロントエンド側の URL(http://192.168.11.9:3000
)をセットしたら、動きました。
# vi .env
#BACKEND_CORS_ORIGINS='["http://localhost:3000","http://127.0.0.1:3000"]'
BACKEND_CORS_ORIGINS='["http://192.168.11.9:3000","http://127.0.0.1:3000"]'
# docker-compose up -d --build backend
以下が BACKEND_CORS_ORIGINS
を参照して、CORS 対策している箇所です。
def setup_cors_middleware(app):
if settings.BACKEND_CORS_ORIGINS:
app.add_middleware(
CORSMiddleware,
allow_origins=[str(origin) for origin in settings.BACKEND_CORS_ORIGINS],
allow_credentials=True,
allow_methods=["*"],
expose_headers=["Content-Range", "Range"],
allow_headers=["Authorization", "Range", "Content-Range"],
)
動作確認
ユーザーの登録と Items の登録ができるようになりました!
画面は、React Admin のダッシュボードで、軽快に動作します。
POST http://192.168.11.9:8000/api/v1/auth/register
POST http://192.168.11.9:8000/api/v1/items
DB にユーザー情報と items 情報が登録されています。
# docker exec -it sample-project_postgres_1 psql -U postgres -d app -c "select * from users;"
id | email | hashed_password | is_active | is_superuser | is_verified | created
| updated
--------------------------------------+--------------------+--------------------------------------------------------------+-----------+--------------+-------------+-----------------------------
--+-------------------------------
1466bd89-fda1-4885-9b70-cf60caf2546f | sample@example.com | $2b$12$j7cCIFbYCSuJ1PdSNtK3WurVf17t/vlfKgSmMHY5wjsW1azExMNtu | t | f | f | 2022-06-24 13:47:08.306265+0
0 | 2022-06-24 13:47:08.306265+00
(1 row)
# docker exec -it sample-project_postgres_1 psql -U postgres -d app -c "select * from items;"
id | user_id | created | updated | value
----+--------------------------------------+-------------------------------+-------------------------------+-------
1 | 1466bd89-fda1-4885-9b70-cf60caf2546f | 2022-06-25 07:34:35.877863+00 | 2022-06-25 07:34:35.877863+00 | aaaaa
(1 row)
コンテナの中に入って見ると、app のテーブル構成は、以下でした。
app=# \d
List of relations
Schema | Name | Type | Owner
--------+-----------------+----------+----------
public | alembic_version | table | postgres
public | items | table | postgres
public | items_id_seq | sequence | postgres
public | users | table | postgres
(4 rows)
pytest を使ったテストにも対応しています。
【 pytest 】
Pythonで書いたプログラムをテストするためのフレームワークです。
# docker-compose exec backend pytest
=========================================== test session starts ===========================================
platform linux -- Python 3.8.13, pytest-7.1.2, pluggy-1.0.0
rootdir: /app
plugins: anyio-3.5.0
collected 9 items
tests/api/test_items.py ....... [ 77%]
tests/api/test_utils.py . [ 88%]
tests/models/test_user.py . [100%]
============================================ 9 passed in 1.55s ============================================
当たり前ですが、無事通りました。
backend 利用パッケージ
backend は、Python で実装されています。
Python パッケージの管理に、Pipfile や requirements.txt
ではなくて、Poetry の pyproject.toml
が使われています。
RUN poetry install --no-root
で Python の依存パッケージをインストールしています。
--no-root
は、自分自身(今回の場合、sample-project
)をインストール対象としないという意味です。
【 Poetry 】
Poetryは、Pythonでの依存関係の管理とパッケージ化のためのツールです。プロジェクトが依存するライブラリを宣言することができ、それらを管理(インストール/更新)します。
pyproject.toml
を読み取って、パッケージをインストールします。Node.js でいう npm あるいは yarn コマンドのような、Ruby でいう bundle コマンドのようなツールです。
pyproject.toml
は、npm、yarn で言うpackage.json
のような存在です。
[tool.poetry] # [...]部分を節という。tool.poetry 節は複数の節で構成されている。
name = "sample-project-backend"
version = "0.1.0"
description = ""
authors = []
[tool.poetry.dependencies] # 本番環境ライブラリ
python = "^3.7"
# python バージョン >=3.7.0 <4.0.0 という意味。
# ^ は、"キャレット要件"と言う。
# 一番左側にある、ゼロでないバージョニングは変えない
# helloworld = "^0.1.2"
# ならば「>=0.1.2 <0.2.0」という意味になる。
# (1がゼロでは無いため、1を維持。)
fastapi = "^0.75.2"
uvicorn = "^0.17.6"
pydantic = "^1.9.1"
requests = "^2.27.1"
alembic = "^1.8.0"
psycopg2-binary = "^2.9.3"
fastapi-users = {extras = ["sqlalchemy2"], version = "^9.3.1"} # extrasで追加オプションを指定。外部パッケージ sqlalchemy2 込み。
asyncpg = "^0.25.0"
SQLAlchemy = "^1.4.37"
databases = "^0.5.5"
gunicorn = "^20.1.0"
[tool.poetry.dev-dependencies] # 開発用ライブラリ poetry install --no-dev で取り除くことができる。
black = "^22.3.0"
pytest = "^7.1.2"
ipython = "^7.34.0"
[build-system] # ビルドシステム。build-backend を指定しなければ、setup.py が実行される。
requires = ["poetry-core>=1.0.0"] # poetry バージョン 1.0.0 以降をインストール
build-backend = "poetry.core.masonry.api" # poetry.masonry.api を実行
[tool.cruft]
skip = [".env"]
# poetry run cruft .
# のように実行可能
【 uvicorn 】
Uvicorn(ユーブイアイコーン)は、Python用の ASGI Web サーバー実装です。
【 ASGI 】
Asynchronous Server Gateway Interfaceの略です。WSGIの後継仕様で、非同期で動作するように設計されています。WebSocketなど複数のプロトコルをサポートしています。
【 WSGI 】
Web Server Gateway Interface(WSGI:ウィスキー)は、WebサーバとWebアプリケーションを接続するための、標準化されたインタフェース定義です。
WSGIで動くプログラムは、WebサーバーがWSGIに対応していれば、無改修で動くという理屈になります。
【 pydantic 】
Python の型アノテーションを用いて実行時にデータのバリデーションを行えるようにするライブラリです。
コード実行時に型の不整合や不正値の場合、例外送出してくれます。(通常は、致命的なエラーにならなければ、例外送出されない。)
【 requests 】
HTTP通信用のPythonのライブラリです。
HTTP通信の実装をPythonの標準ライブラリurllibよりシンプルに書けます。
【 alembic 】
Alembic(アランビック)=蒸留器
Python で SQLAlchemy を使用しているときに DB の管理をしてくれる migration ツールです。
マイグレーションとは、DBに保存されているデータを保持したまま、テーブルの作成やカラムの変更などを行うための機能です。
マイグレーション機能を使うことで特定のデータベースに依存せずにデータベースの操作ができます。
【 psycopg2-binary 】
Psycopgは、Pythonプログラミング言語用のPostgreSQLデータベースアダプターです。
その主な機能は、Python DB API 2.0仕様の完全な実装とスレッドセーフです(複数のスレッドが同じ接続を共有できます)。
(pysopg2とpsycopg2-binaryの違い:コンパイラや外部のライブラリ等を必要としないのが-binaryの方です。libpqを内包しています。)
【 fastapi-users 】
FastAPIプロジェクトに登録および認証システムをすばやく追加します。
FastAPIにJWT認証、Cookie認証の機能を提供します。(Python3.7以降が必要。)
(サポートしているORM:SQLAlchemy, MongoDB, Tortorise ORM)
【 asyncpg 】
PythonからPostgreSQLにアクセスするためのライブラリです。
asyncpgはasyncio(非同期 I/O。 async/await 構文を使い 並行処理の コードを書くためのライブラリ。)をベースに動きます。
非同期処理をベースにしているためか、パフォーマンスが良いです。
【 SQLAlchemy 】
Alchemy(アルケミィ)=錬金術
Pythonで利用されるORMの1つです。
SQLAlchemyではコマンドでの操作やAlter Tableなどは行えませんが、Alembicではコマンドを用いたスキーマの操作やAlter Tableを行うことができます。
【 databases 】
さまざまなデータベースのシンプルなasyncioサポートを提供します。
databasesは、Starlette、Sanic、Responder、Quart、aiohttp、Tornado、FastAPIなどの非同期Webフレームワークとの統合に適しています。
【 gunicorn 】
Gunicorn(グニコーン)は、WSGI アプリケーション用の Python HTTP サーバーです。
Gunicornサーバーは、多数のWebフレームワークと広く互換性があり、実装がシンプルで、サーバーリソースが少なく、高速に動作します。
【 ipython 】
IPython(アイパイソン)はPythonを対話的に実行するためのシェルです。
Pythonの対話型インタプリタを拡張したものです。
コードのシンタックスハイライトおよびタブによる補完が行えます。
【 cruft 】
cruftを使用すると、意図的に記述したコードとは別に、プロジェクトのパッケージ化と構築に必要なすべての定型文を維持できます。
既存のCookiecutterテンプレートと互換性があります。
(Cookiecutterと違い、テンプレートが変更されたら、反映することができるもののようです。)
frontend 利用パッケージ
frontend は、node.js React TypeScript で実装されています。
frontend の方は、React & react-admin
がメインで、これといって紐解くものが無いので、package.json
の scripts の部分だけ少し言及します。
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"test:debug": "react-scripts --inspect-brk test --runInBand --no-cache",
"eject": "react-scripts eject",
"format": "prettier src --write",
"run-e2e-tests": "cypress run",
"open-e2e-test": "cypress open",
"genapi": "openapi-generator-cli generate -i <(curl -s 'http://localhost:8000/api/v1/openapi.json') -g typescript-axios -o src/generated -p withSepa
},
今回、実行環境を作らないといけないため、実行していませんが、cypress でのテスト用スクリプトも入っています。
【 Cypress 】
ブラウザでのテスト作業を自動化するテストフレームワークです。React の E2Eテストを自動化できます。
【 E2Eテスト 】
E2Eテストとは、「End To Endテスト」の略であり、ユーザが利用するのと同じようにシステム全体をテストします。
ユーザと同じようにブラウザを操作し、挙動が期待通りになっているか確認します。
また、openapi-generator-cli
を使って、API の実装を自動化しています。(yarn run genapi
と実行すると、API の実装が作成されます。)
java を使っているため、java をインストールしています。
# cd frontend
# apk add yarn curl openjdk11
# yarn install
# yarn run genapi
yarn run v1.22.19
$ openapi-generator-cli generate -i <(curl -s 'http://localhost:8000/api/v1/openapi.json') -g typescript-axios -o src/generated -p withSeparateModelsAndApi=true,apiPackage=api,modelPackage=models,useSingleRequestParameter=true
・・・
[main] INFO o.o.codegen.TemplateManager - writing file /home/admin/sample-project/frontend/src/generated/models/bearer-response.ts
[main] INFO o.o.codegen.TemplateManager - writing file /home/admin/sample-project/frontend/src/generated/models/error-model.ts
[main] INFO o.o.codegen.TemplateManager - writing file /home/admin/sample-project/frontend/src/generated/models/httpvalidation-error.ts
[main] INFO o.o.codegen.TemplateManager - writing file /home/admin/sample-project/frontend/src/generated/models/item.ts
[main] INFO o.o.codegen.TemplateManager - writing file /home/admin/sample-project/frontend/src/generated/models/item-create.ts
[main] INFO o.o.codegen.TemplateManager - writing file /home/admin/sample-project/frontend/src/generated/models/item-update.ts
[main] INFO o.o.codegen.TemplateManager - writing file /home/admin/sample-project/frontend/src/generated/models/user.ts
・・・
OpenAPI 仕様(openapi.json
)に合わせたコードが自動生成されました。
【 OpenAPI 】
OpenAPI Specification とは、REST APIの仕様を記述するためのフォーマットのことです。
YAML または JSON形式で定義します。
【 OpenAPI Generator 】
OpenAPI で書かれたドキュメントから、クライアントコードを自動で生成してくれるツールです。
構成紐解き
docker-compose up
で、docker-compose.yml
と docker-compose.override.yml
が使われます。(docker-compose の仕様で、自動で docker-compose.override.yml
も読み込まれます。)
backend/Dockerfile
、fronend/Dockerfile
にビルドと起動方法が書かれています。
version: "3.9"
services:
postgres:
image: postgres:12
restart: always
env_file:
- .env
healthcheck:
test: pg_isready -U postgres
# pg_isready : PostgreSQLデータベースサーバの接続状態を検査するためのユーティリティ
interval: 3s
# 3秒に一回ヘルスチェック
timeout: 2s
# 2秒応答が無かったら、タイムアウト判定
retries: 3
# 3回連続 成功/失敗 で healthy/unhealthy に移行
backend:
build:
context: backend
# ビルドコンテキスト(ビルド環境)=backend ディレクトリ。backend/Dockerfile を使用。
ports:
- "8000:8000"
env_file: .env
depends_on:
postgres:
condition: service_healthy
# service_healthy : 依存サービスのヘルスチェックがパスするまで待つ
frontend:
build:
context: frontend
# ビルドコンテキスト(ビルド環境)=frontend ディレクトリ。frontend/Dockerfile を使用。
ports:
- "3000:80"
volumes:
- ./frontend:/app:delegated
# delegated : 一貫性に対する保証が最も弱いが、最も高速。コンテナ上の更新がホスト上に反映するまで、遅延が発生するのを許容。
environment:
- NODE_ENV=production
volumes:
app-db-data:
root-home:
version: "3.9"
services:
postgres:
ports:
- "5432:5432"
volumes:
- app-db-data:/var/lib/postgresql/data:cached
# cached : ホストの表示が信頼できる。ホスト上の更新がコンテナ上に反映するまで、遅延が発生するのを許容
backend:
command: uvicorn --port 8000 --host 0.0.0.0 --reload main:app
# backend/Dockerfile の CMD より優先(同じコマンドが書かれている。)
volumes:
- ./backend:/app:delegated
- root-home:/root:delegated
depends_on:
postgres:
condition: service_healthy
docker-compose.ci.yml というのがありますが、
Github Actions 設定ファイルの一つ .github/workflows/test.yaml
に
docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d
docker-compose exec -T postgres createdb -U postgres apptest
docker-compose exec -T backend alembic upgrade head
docker-compose exec -T backend pytest -v
が書かれていて、テストに使われるようです。 (Github Actions は今回検証しませんでした。)
version: "3.9"
services:
backend:
image: "sample-project-backend:latest"
frontend:
image: "sample-project-frontend:latest"
frontend 側の Dockerfile、frontend/Dockerfile
では、
Docker multi stage build の手法が使われていて、node:16
イメージでビルドした後、nginx:latest
イメージにデプロイしています。
nginx の設定は、frontend/nginx.conf
で、ビルド済みファイル配置先の /usr/share/nginx/html
をドキュメントルートとしています。
FROM node:16 as build
WORKDIR /app
COPY package.json yarn.lock /app/
RUN yarn
COPY . /app/
RUN CI=1 yarn test
RUN yarn build
FROM nginx:latest
COPY /app/build /usr/share/nginx/html
COPY /app/nginx.conf /etc/nginx/conf.d/default.conf
以上!
その他、宣伝、誹謗中傷等、当方が不適切と判断した書き込みは、理由の如何を問わず、投稿者に断りなく削除します。
書き込み内容について、一切の責任を負いません。
このコメント機能は、予告無く廃止する可能性があります。ご了承ください。
コメントの削除をご依頼の場合はTwitterのDM等でご連絡ください。