初心者がFastAPI + React + Dockerで開発するときに考えたこと

# はじめに
FastAPI, React, Docker...どれも調べ調べに身につけて歴4ヶ月の者です。
今回は私流ではありますが、FastAPI + React + Dockerで開発するときに考えたことをまとめたいと思います。

### なぜFastAPIなのか？
FastAPIは、非同期処理を得意としたPythonのAPI構築webフレームワークです。PythonにはsklearnやPytorchなど機械学習に特化したライブラリーが揃っているので、バックエンドに機械学習を盛り込みたいという場合はFastAPIは有効だと考えてます。(今回は盛り込みませんが...)

### なぜDockerを使うのか？
バックエンドとフロントエンドでサーバーが分離している場合、dockerは有効になります。異なるコンテナを同時に起動させて中継することができるので、非常に便利です。中継がないと、デプロイ時にサーバーが２つ必要になるので、そういう点でも重要です。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も入ってます。)

https://github.com/pdmuds4/react_envTemplate


# 2. FastAPI関連について
`run.py`は以下のようになっています。
* ポイントとなるのは、CORSを使うことです。
Reactが`port:3000`、FastAPIが`port:3001`を使うとすると、FastAPIは`port:3000`からのアクセスを許可する必要があります。ちなみにですがリバースプロキシを使うと許可を出さずともアクセスできるようになります。

* こちらはプログラムベースでサーバーを起動しています。コマンドラインで`uvicorn`を実行しても構いませんが、テストの度にコマンドを入れるのは個人的に面倒だと感じています。

```python:run.py
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](https://mui.com/)、コンポーネントベースでスタイリングされたテンプレートを使用することができます。
    * `axios`：APIとのデータのやりとりを行うためのライブラリです。
    * `node-sass`：静的sassファイルからスタイリングを実装する場合に使います。
    * `react-router-dom`：Reactでルーティングの定義をするために使います。（単体では定義ができません。）

これらについて、`axios`での通信と、`react-router-dom`の方を説明します。

```json:package.json
{
  "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()`インスタンスを入れることで送信することができます。

```javascript:response.js
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`コンポーネントが表示されるということです。

```react:index.js
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へコピーしています。

```Dockerfile:Dockerfile
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`があると確実です。

```Dockerfile:Dockerfile
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"]
```

```tex:requirements:txt
fastapi
uvicorn
python-multipart
```

### docker-compose.yml
* 2つのコンテナを稼働します。イメージ名は`-backend`と`-frontend`とします。
* `ports:`を設定して、それぞれのポート番号を設定しておきます。

```yaml:docker-compose.yml
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がいいかなと思う。

# おわりに
今回は個人的な観念でだらだらと書かせていただきました。
所詮初心者の考えることなので、さわり前の人とかは参考程度に受け止めてくださると嬉しいです。プロフェッショナルな方で、もし改善点などありましたらコメントお待ちしております。