はじめに
FastAPI, React, Docker...どれも調べ調べに身につけて歴4ヶ月の者です。
今回は私流ではありますが、FastAPI + React + Dockerで開発するときに考えたことをまとめたいと思います。
なぜFastAPIなのか?
FastAPIは、非同期処理を得意としたPythonのAPI構築webフレームワークです。PythonにはsklearnやPytorchなど機械学習に特化したライブラリーが揃っているので、バックエンドに機械学習を盛り込みたいという場合はFastAPIは有効だと考えてます。(今回は盛り込みませんが...)
なぜDockerを使うのか?
バックエンドとフロントエンドでサーバーが分離している場合、dockerは有効になります。異なるコンテナを同時に起動させて中継することができるので、非常に便利です。中継がないと、デプロイ時にサーバーが2つ必要になるので、そういう点でも重要です。nginxなどでリバースプロキシを使うという手もありますが、今回はdockerでやっていきます。
1. ディレクトリ構造
プロジェクトごとに変えることなく基本的に統一させたほうがよいと感じました。一例ではありますが、私はこんな感じのテンプレートみたいなものを作って、必要な箇所を毎回使いまわしています。
# ---Docker関連--- #
├── docker
│ ├── backend
│ │ ├── Dockerfile
│ │ └── requirements.txt
│ └── frontend
│ └── Dockerfile
├── docker-compose.yml
│
│
# ---React関連--- #
├── package-lock.json
├── package.json
├── public
│ └── index.html
├── src
│ ├── components # 画面ごとに必要なコンポーネントを入れる
│ │ └── component-1
│ │ │ ├── child-component # 子コンポーネントを同じディレクトリに
│ │ │ │ ├── child-component.module.scss
│ │ │ │ └── index.jsx
│ │ │ ├── component-1.module.scss
│ │ │ └── index.jsx
│ ├── pages
│ │ └── page.jsx # 画面遷移ごとのコンポーネントを入れる
│ ├── index.js # ルーティングとレンダリングを定義する
│ └── response.js # axiosでAPIとのやりとりをする
│
│
# ---FastAPI関連--- #
├── run.py # fastapiメイン
├── apis
│ └── page.py # apiのパスごとにソースを分ける
└── scripts
├── crud # データベースへのクエリー関数
│ └── user.py
├── models # テーブルスキーマを定義するモデル
│ └── User.py
└── other.py # その他の機能
これが正解とはいえませんが、個人的にはこれがやりやすい環境かなと感じています。一部欠けている部分はありますが、githubにこのテンプレートを保存しているのでよかったら使ってみてください。(node.js用にexpressも入ってます。)
2. FastAPI関連について
run.py
は以下のようになっています。
-
ポイントとなるのは、CORSを使うことです。
Reactがport:3000
、FastAPIがport:3001
を使うとすると、FastAPIはport:3000
からのアクセスを許可する必要があります。ちなみにですがリバースプロキシを使うと許可を出さずともアクセスできるようになります。 -
こちらはプログラムベースでサーバーを起動しています。コマンドラインで
uvicorn
を実行しても構いませんが、テストの度にコマンドを入れるのは個人的に面倒だと感じています。
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import uvicorn
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/")
async def index():
return {"message": 'Hello World!'}
if __name__=="__main__":
uvicorn.run("run:app",port=3001, reload=True)
3. React関連について
package.json
の先頭は以下のようになっています。
-
port:3001
で起動するように、proxy
を設定しています。 - インストールしているパッケージですが、私がよく使うものを入れてます
-
emotion
:jsx内でスタイリングができるライブラリです。 -
mui
:Material UI、コンポーネントベースでスタイリングされたテンプレートを使用することができます。 -
axios
:APIとのデータのやりとりを行うためのライブラリです。 -
node-sass
:静的sassファイルからスタイリングを実装する場合に使います。 -
react-router-dom
:Reactでルーティングの定義をするために使います。(単体では定義ができません。)
-
これらについて、axios
での通信と、react-router-dom
の方を説明します。
{
"name": "react_test",
"version": "0.1.0",
"private": true,
"proxy": "http://localhost:3001",
"dependencies": {
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.14.15",
"@mui/joy": "^5.0.0-beta.12",
"@mui/material": "^5.14.15",
"@mui/x-charts": "^6.0.0-alpha.17",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"axios": "^1.5.1",
"node-sass": "^7.0.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.15.0",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},
axiosでAPIとやりとりをする
src/response.js
では以下のようになっています。
- ローカルで作業をする場合とdockerで動かす場合とでドメインを分けています。
- 関数の引数にエンドポイントを入れるだけでデータを取得できるように関数を用意しています。例えば、
getResponse("/hello")
といれると、Fastapiで@app.get("/hello")
と定義されているAPIからデータを受け取ることができます。 -
postResponse()
は、第二引数にdata
をとるようになっていますが、データをFormData()
インスタンスを入れることで送信することができます。
import axios from "axios";
const Domain = "http://127.0.0.1:3001";
// docker用
//const Domain = "http://0.0.0.0:3001";
// get methodでのレスポンスを返す関数
const getResponse = async (endpoint) => {
try {
const res = await axios.get(Domain + endpoint);
return res.data;
} catch (e) {
throw e;
}
}
// post methodでデータを送信し,レスポンスを返す関数
const postResponse = async (endpoint, data) => {
try {
const res = await axios.post(Domain + endpoint, data);
return res.data;
} catch (e) {
throw e;
}
}
Reactでルーティングとレンダリングを定義する
src/index.js
では以下のようになっています。
- Reactではルーティングの定義はサポートされていないので、
react-router-dom
を使って定義しています。 -
src/pages
にルートごとのコンポーネントを定義し、<Routes>
に組み込みます。 - ルートは
<Route>
を使って定義します。以下の例だと、/
にアクセスした時にPage
コンポーネントが表示されるということです。
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Page from './pages/page';
function App() {
return (
<Routes>
<Route exact path="/" element={ <Page /> }/>
</Routes>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
);
4. Docker関連について
Frontend
docker/frontend/Dockerfile
は以下のようになっています。
-
nodejs
ベースのイメージを使っています。 - ソースファイルをそのままの形でdockerへコピーしています。
FROM node:latest
WORKDIR /frontend
COPY package.json .
RUN npm install
COPY /src /frontend/src/
COPY /public /frontend/public/
CMD ["npm", "start"]
Backend
docker/backend/Dockerfile
とdocker/backend/requirements.txt
は以下のようになっています。
- pythonバージョンはローカル環境と同じものを使うことを推奨します。
-
requirements.txt
を用意し、pip install
できるようにしておきます。 - インストール前に
pip
バージョンのアップグレードをしておくことを推奨します。 - 最新のパッケージバージョンを取得したい場合は、
--no-cache-dir
があると確実です。
FROM python:3.10-alpine
WORKDIR /backend
COPY /docker/backend/requirements.txt .
RUN pip install --no-cache-dir --upgrade -r /backend/requirements.txt
COPY run.py /backend/
COPY /scripts /backend/scripts
CMD ["uvicorn", "run:app", "--reload", "--host", "0.0.0.0", "--port", "3001"]
fastapi
uvicorn
python-multipart
docker-compose.yml
- 2つのコンテナを稼働します。イメージ名は
-backend
と-frontend
とします。 -
ports:
を設定して、それぞれのポート番号を設定しておきます。
version: '3'
services:
backend:
build:
context: .
dockerfile: docker/backend/Dockerfile
ports:
- 3001:3001
frontend:
build:
context: .
dockerfile: docker/frontend/Dockerfile
ports:
- 3000:3000
5. 他思ったこと
- nginxでリバースプロキシをやりたい!
- tsベースで使ったほうが良い気がした。
- 細かいスタイリングはemotionよりTailwind CSSがいいかなと思う。
おわりに
今回は個人的な観念でだらだらと書かせていただきました。
所詮初心者の考えることなので、さわり前の人とかは参考程度に受け止めてくださると嬉しいです。プロフェッショナルな方で、もし改善点などありましたらコメントお待ちしております。