【Day12】FastAPIのコードをリファクタリングする

## はじめに

勉強とアウトプットを兼ねて、Webアプリ開発の記録を記事にしてみます。

個人のメモ的な側面が強いです。
Web開発初心者すぎて至らぬところだらけだと思いますが、暖かい目で見ていただければ幸いです。

↓ これまでの内容 ↓
[【Day0】アプリが完成するまで毎日投稿](https://qiita.com/midorigame/items/f9b9a445c945b4a837b1)
[【Day1】開発環境を考える](https://qiita.com/midorigame/items/e26344599e2ffaa22033)
[【Day2】開発環境を構築する、、、はずでした](https://qiita.com/midorigame/items/3f57f2691a8e1074adb8)
[【Day3】まずはフロントだけ開発環境を構築する](https://qiita.com/midorigame/items/9de27ba72aaef504a833)
[【Day4】バックエンドの開発環境を整えつつ、マルチコンテナ対応のDevcontainerにする](https://qiita.com/midorigame/items/5b95374cd85f715a6498)
[【Day5】お試し実装！ファイルをアップロードしてバックエンドで処理する](https://qiita.com/midorigame/items/9663f0063b95b501c08a)
[【Day6】Docker Desktopが起動しなくなり泣きながら原因調査する](https://qiita.com/midorigame/items/d07df1caf05267d0609d)
[【Day7】画像にフレームとExif情報を付けてみる](https://qiita.com/midorigame/items/e2d85ce8dd7a5bcb5daf)
[【Day8】フロントに表示する画像のサイズを小さくする](https://qiita.com/midorigame/items/f7f5f9b14fb79259c716)
[【Day9】フロントエンドとバックエンドをそれぞれデプロイする](https://qiita.com/midorigame/items/4c8037024e667ff1105b)
[【Day10】結局コンテナ1つで開発する方が楽かもしれない](https://qiita.com/midorigame/items/60b484b2007c3aab474b)
[【Day11】アプリが一通り完成！！](https://qiita.com/midorigame/items/38ddccc09f0625aaebd6)

## 前回やったこと

作りたいアプリが一旦完成しました！
ここからは追加機能を作ったり、ソースコードを手入れしたりしようかなと思います。

## 今回やったこと

Chat GPTに聞いたコードをぺたぺた張り付けて動かしている状態なので、だいぶスパゲッティなソースコードになってます。。

なので、可読性も上げつつ、自分の理解も深めるためにリファクタリングしてみました。

## リファクタリング

### 現在のディレクトリ構成

今は2つのファイルだけで実装していました。
`image_editor.py`に画像処理を全部詰め込んでいるせいで、だいぶカオスな状態になってました。

```
project_root/
├── app/
│   ├── main.py
│   └── image_editor.py
├── requirements.txt
└── README.md
```

### Chat GPTに相談

FastAPIを使う上でのデファクトスタンダードなディレクトリ構成をChat GPTに聞いてみました。

```text:回答（一部抜粋）
✅ 推奨ディレクトリ構成（小中規模用）
project_root/
├── app/
│   ├── api/
│   │   └── v1/
│   │       └── endpoints/
│   │           └── upload.py         ← /upload-image のルート（FastAPI ルーター）
│   ├── core/
│   │   └── config.py                 ← 設定管理（環境変数など）
│   ├── services/
│   │   └── image_processor.py       ← 画像加工のロジック（util系をここに）
│   ├── main.py                      ← アプリの起動ポイント
│   └── deps/                        ← 依存注入（必要な場合のみ）
├── tests/
│   └── test_upload.py               ← pytest 用のテスト
├── requirements.txt
└── README.md

🔍 各フォルダの意味と役割
| ディレクトリ/ファイル             | 用途・意味                                         |
| ----------------------- | --------------------------------------------- |
| `app/main.py`           | FastAPI アプリのエントリーポイント（`uvicorn app.main:app`） |
| `app/api/v1/endpoints/` | エンドポイント群（APIごとにファイルを分ける）                      |
| `app/services/`         | **ビジネスロジックやユーティリティ** を置く場所（画像処理など）            |
| `app/core/`             | 設定ファイルやアプリの共通設定                               |
| `tests/`                | テスト用コード（pytest 推奨）                            |

👇 あなたのプロジェクトに即した具体例
/upload-image のロジック → app/api/v1/endpoints/upload.py
画像加工関数（resizeやExif関連）→ app/services/image_processor.py
```

なるほど！
めちゃくちゃ丁寧に教えてもらいました。

さらに画像加工用の関数をまとめたファイルをどこに配置すればいいかも聞いてみました。

```text:回答
1. 責務ごとに関数をまとめる
大きく分けて以下のような責務に分けられるはずです：

| グループ         | 内容の例                |
| ------------ | ------------------- |
| **画像リサイズ処理** | 縦横サイズの調整、比率保持       |
| **Exif処理**   | Exifの抽出、保存、再付与      |
| **合成処理**     | フレーム合成、透過処理など       |
| **ユーティリティ**  | 拡張子チェック、MIMEタイプ確認など |


2. モジュールを小分けにする（例）
以下のような構成に分割できます。

app/
└── services/
    ├── image_processor/
    │   ├── __init__.py               ← 各関数のまとめ（必要に応じて）
    │   ├── resize.py                 ← サイズ調整処理
    │   ├── exif_utils.py             ← Exifの読み取り・保持
    │   ├── compose.py                ← フレームとの合成やエフェクト
    │   └── utils.py                  ← 汎用ユーティリティ関数
    └── image_processor.py           ← ※必要に応じて旧コード残しつつ移行
```

なるほど。こちらも良い感じにディレクトリ分割できそうですね。

### 最終的なディレクトリ構成

Chat GPTからの回答を踏まえて、以下のようなディレクトリ構成にしました。
実装を役割ごとにファイル/ディレクトリ分割したことで全体的にすっきりしました！どのファイルも100行以上ロジックを書いてないため、可読性もだいぶ高まったかなと思います。

```text
project_root/
├── app/
│   ├── api/
│   │   └── v1/
│   │       └── endpoints/
│   │           └── upload.py
│   ├── core/
│   │   └── config.py 
│   ├── services/
│   │   └── image_processor/
│   │       ├── __init__.py
│   │       ├── exif_utils.py
│   │       ├── frame_composer.py
│   │       ├── image_drawer.py
│   │       └── text_drawer.py
│   └── main.py
├── requirements.txt
└── README.md
```

上記のディレクトリ構成でポイントになる4つのファイルだけ簡単に役割を説明します。
1. main.py
1. config.py 
1. upload.py
1. frame_composer.py

#### 1. main.py

mainではルート（ / ）以外のAPI定義はせずに、FastAPIの起動と設定追加だけにしました。

```python:main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.api.v1.endpoints import upload
from app.core.config import get_allow_origin_list


app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=get_allow_origin_list(),
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

app.include_router(upload.router, prefix="/v1")

@app.get("/")
def read_root():
    return {"message": "Hello, FastAPI!"}
```

#### 2. config.py

FastAPIの設定に関わる部分を記述します。
今回はCORSの設定を書きました。

この部分はもっといい書き方ができる気がするけど、一旦これで行きます！

```python:config.py
import os


def get_allow_origin_list():
    allow_origin_list = ["http://localhost:3000"]

    try:
        origin = os.environ.get('ALLOW_ORIGIN')
        if origin:
            allow_origin_list.append(origin)
        print(f"ALLOW_ORIGIN : {allow_origin_list}")
    except Exception as e:
        print("ALLOW_ORIGIN is not found.")
        print(e)

    return allow_origin_list
```

#### 3．upload.py

今回のアプリのメイン機能であるアップロードされた画像にフレームを付与して返すAPIを定義しています。

ここでは詳細な画像処理は行わず、画像加工はすべて`from  app.services.image_processor import frame_composer`に任せています。

```python:upload.py
from fastapi import FastAPI, File, UploadFile, Form, APIRouter
from fastapi.responses import StreamingResponse
import io
from PIL import Image
from  app.services.image_processor import frame_composer


router = APIRouter()


@router.post("/upload-image")
async def upload_image(
        file: UploadFile = File(...),
        show_exif: str = Form("true")  # ← str型で受け取る
    ):
    show_exif_flag: bool = show_exif.lower() == "true"

    image = Image.open(file.file)
    edited_image = frame_composer.create_framed_image(image, show_exif_flag)

    buffer = io.BytesIO()
    edited_image.save(buffer, format="PNG")
    buffer.seek(0)
    return StreamingResponse(buffer, media_type="image/png")
```

#### 4. frame_composer.py

実際に画像にフレームを付けている部分の実装になります。
ただし、画像の処理やテキストの処理はさらに細かい単位でファイル分割しています。

```python:frame_composer.py
from PIL import Image
from .image_drawer import draw_image_area_on_frame
from .text_drawer import draw_test_area_on_frame
from .exif_utils import extract_exif


def create_framed_image(image: Image.Image, is_use_exif: bool) -> Image.Image:
    # フレーム全体のサイズ
    frame_width = 1080
    frame_height = 1350
    if is_use_exif:    
        image_area_height = 1200
        text_area_height = 150
    else:
        image_area_height = 1350
        text_area_height = 0 

    # 新しい背景画像（白背景）
    frame = Image.new("RGB", (frame_width, frame_height), color=(255, 255, 255))

    # フレームに画像を描画
    draw_image_area_on_frame(image, image_area_height, frame, frame_width, frame_height)

    if is_use_exif:
        exif = extract_exif(image)
        print(exif)
        draw_test_area_on_frame(frame, exif)

    return frame
```

### 本日のコミット

他にも書いたコードや全体のディレクトリ構成が気になる方がいれば、こちらのコミットを見てみてください。（記事執筆時点では公開中）

https://github.com/midorigameee/React-Image-Generator/commit/d61ba5cc7995ddd9431fd1fb8f83b3482bbca758

## おわりに

今回はリファクタリングだけで終わってしまいました。本当はテストコードも書いてみたかったですが、それはまた後程！

正直個人開発なので動けばいいやん、、とも思うかもしれないですが、せっかく自分で好き勝手できるので、これを機に純粋な実装以外の知識も身に着けていきたいですね。