application/json｜multipart/form-dataどっちにしようか？

こんにちは:sunny:

株式会社HRBrainでフロントエンジニア（とバックエンドエンジニア？）をしているみつです！

スキーマを考えている時に、`application/json`と`multipart/form-data`のどちらにするかで悩みました。

最終的には、`application/json`と`multipart/form-data`それぞれにメリット・デメリットが存在するため、場面に応じた使い方をしないといけないな！となった過程を残しておきたいと思い記事にしました！

## 背景
- IDに紐づくPDF等のドキュメントファイル（以降、ドキュメントと記載）をアップロードしたい

## 前提
- ドキュメントは複数送る可能性がある
- 1つあたりのドキュメントアップロード上限は、20MB〜30MB程度とする
- フロントエンドのコード生成には、`orval`というライブラリを使用している

を満たすスキーマを検討していました。

## 検討した方法（4案）
検討した方法は、4案でした。
- **[1案] APIは1本で、`application/json`でデータを送信（ドキュメントは`base64`に変換）**
- **[2案] APIは1本で、`multipart/form-data`でデータを送信（ドキュメントは`binary`のまま送信）**
- **[3案] APIを2本作り、`application/json`と`multipart/form-data`でそれぞれ送る**
- **[4案] APIを2本作り、どちらも`application/json`で送る**

それぞれのメリット・デメリットも考えてみます。

### [1案] APIは1本で、`application/json`でデータをすべて送信（ドキュメントは`base64`に変換）

#### メリット
:::note info
- 1つのAPIですべてを送ることができる（1本だけで良いので、2本作るより楽）
:::
#### デメリット
:::note warn
- 1つのAPIですべて送信することになる
  - ↑ ファイルが複数ある場合、実行時間が伸びていきそう
- Base64へファイルをエンコード、デコードするため処理時間が余分にかかる
- エンコードすることでファイルサイズが増えるため通信時間も増える
:::
#### [参考記事]

https://techblog.imagemagic.jp/2023/06/12/ファイルアップロードを伴うwebapiの設計パターンを/

### [2案] APIは1本で、`multipart/form-data`でデータをすべて送信（ドキュメントは`binary`のまま送信）
#### メリット
:::note info
- 1つのAPIですべてを送ることができる（1本だけで良いので、2本作るより楽）
- ファイルを`binary`のまま送ることができる
  - ↑ `application/json`と比較して、処理時間やファイルサイズの心配をしなくて良くなる
:::
### デメリット
:::note warn
- 1つのAPIですべて送信することになる
  - ↑ ファイルが複数ある場合、実行時間が伸びていきそう
- `multipart/form-data`は、フォームとして送ることになる
  - ↑ keyの設定などで複雑になる可能性がある
:::
### [3案] APIを2本作り、`application/json`と`multipart/form-data`でそれぞれ送る
- `application/json` → それ以外をアップロードする
- `multipart/form-data` → ドキュメントをアップロードする

#### メリット
:::note info
- ドキュメントは`binary`のまま送ることができる
- それ以外は基本的な`json`形式のまま送ることができる
- APIが2本に分かれているのでドキュメントアップロードだけ非同期でCloud Run Jobに任せるなどの選択肢が取れる
:::

#### デメリット
:::note warn
- APIが2本になるため開発コストが高くなる
:::


### [4案] APIを2本作り、どちらも`application/json`で送る
- `application/json` → ドキュメントをアップロードするAPI
- `application/json` → それ以外をアップロードするAPI

#### メリット
:::note info
- ドキュメントはエンコード、デコードが必要だが、それぞれに責務を分割できているためコードの見通しがよくなる
- APIが2本に分かれているのでドキュメントアップロードだけ非同期でCloud Run Jobに任せるなどの選択肢が取れる
:::

#### デメリット
:::note warn
- APIが2本になるため開発コストが高くなる
- 「ファイルのエンコード、デコードで処理時間が余分にかかる」「Base64エンコードすることでファイルサイズが増えるため通信時間も増える」は問題として残る
:::

## 先輩エンジニアに聞いてみる

### 先輩からのアドバイス①
- スピードがそこまで求められるものではないので、どちらでも良いと思う
- 色々な選択肢があると思うが、フロントエンドが実装しやすい方に合わせて上げるというのも1つの選択肢
- ファイルはファイルのまま（`binary`で）送るのも楽といえば楽

### 先輩からのアドバイス②
- ドキュメントは複数ある可能性もあるため、ファイルサイズもどんどん増える
  - ↑ であれば、API自体を分けた方が良い可能性もある
- 加えてそもそも、`base64`で一緒に送らないといけない理由はあるんだっけ
  - ↑ `base64`にする必要がないなら、わざわざ`base64`にしなくても

## 決まった方向性
それぞれにメリット・デメリットはあるようでした。

そして、`base64`に変換する必要がなければわざわざ変換しなくても良いのではないかというアドバイスはその通りだなと感じ、フロントからバックエンドに送る時点では`binary`のまま取り扱うことが良いという判断をしました。

結果的に、APIが2本あるためデータベースに保存が必要なものは保存し、時間のかかる処理は非同期で処理するなどの選択肢も取りやすくなりました。

## 処理の流れ
```mermaid
sequenceDiagram
    participant フロントエンド
    participant バックエンド
    participant データベース
    participant GCS

    フロントエンド->>バックエンド: binary以外の情報をアップロード(application/json)
    フロントエンド->>バックエンド: binaryアップロード(multipart/form-data)
    バックエンド->>バックエンド: IDを発行
    バックエンド->>GCS: binaryをIDから作成したバケットにアップロード（非同期）
    バックエンド->>データベース: binary以外の情報を保存
    バックエンド-->>フロントエンド: リソースIDを返す。
    GCS-->>バックエンド: 完了通知
```

## まとめ
難しいなぁと思いながら、ない頭をひねって最終的にAPIを2本で実装するという方向性を固めることができました。

他に良い実装があればアドバイスください🙇‍♂️

おわり。

## 参考

https://orval.dev/reference/configuration/output#formdata

https://techblog.imagemagic.jp/2023/06/12/ファイルアップロードを伴うwebapiの設計パターンを/


## PR
株式会社HRBrainでは、一緒に働く仲間を募集しています！

興味を持っていただいた方はぜひ弊社の採用ページをご確認ください！

HRBrain文化を一緒に作っていきましょう！

https://www.hrbrain.co.jp/recruit/