この記事の最終更新日: 2026年6月8日

REST APIを設計していると、意外と悩みやすいのが ファイルアップロード です。
ユーザー情報や記事データのようなJSONだけを扱うAPIなら、比較的シンプルに設計できます。
POST /users
Content-Type: application/json
{
"name": "山田太郎",
"email": "yamada@example.com"
}
しかし、画像・PDF・音声・CSV・動画などのファイルを扱う場合は、設計で考えることが一気に増えます。
たとえば、次のような疑問が出てきます。
ファイルはJSONで送るの?
multipart/form-dataを使うべき?
S3に直接アップロードするべき?
ファイル情報はDBに保存するべき?
アップロード後のURLはどう返すべき?
画像差し替えや削除はどう設計する?
セキュリティ面で何に注意するべき?
この記事では、REST APIでファイルアップロードを設計するときの考え方を、具体例を使って解説します。
- ファイルアップロードAPIは普通のJSON APIとは少し違う
- 方式1:multipart/form-dataでAPIサーバーにアップロードする
- multipart/form-data方式のメリット
- multipart/form-data方式のデメリット
- 方式2:署名付きURLでストレージに直接アップロードする
- 署名付きURL方式のメリット
- 署名付きURL方式のデメリット
- どちらの方式を選ぶべきか
- ファイルアップロードAPIの基本設計
- 例1:プロフィール画像をアップロードするAPI
- 例2:記事に画像を添付するAPI
- 例3:アップロード済みファイル一覧を取得するAPI
- 例4:ファイルを削除するAPI
- 例5:ファイルを差し替えるAPI
- ファイル情報はDBに保存するべきか
- DBに保存しておきたい情報
- ファイルURLをそのまま返してよいか
- ファイルアップロードAPIのセキュリティ対策
- 1. ファイルサイズ制限をかける
- 2. MIMEタイプを検証する
- 3. 実行可能ファイルをアップロードさせない
- 4. ファイル名をそのまま保存パスに使わない
- 5. 認可チェックを必ず行う
- 6. ウイルススキャンを検討する
- アップロード後に非同期処理が必要な場合
- 署名付きURL方式のAPI設計例
- 1. アップロードURLを発行する
- 2. クライアントがストレージへ直接アップロードする
- 3. アップロード完了をAPIに通知する
- 署名付きURL方式で注意すべきこと
- ファイルアップロードのエンドポイント設計例
- ファイルをリソースとして考える
- ファイルアップロードで使うHTTPメソッド
- multipart/form-dataでメタ情報も一緒に送る場合
- 複数ファイルアップロードの設計
- ファイルアップロードのステータスコード
- ファイルアップロードでよくある悪い設計
- 悪い例1:Base64でJSONに埋め込む
- 悪い例2:ファイルをDBに直接保存する
- 悪い例3:公開してはいけないファイルを公開URLで返す
- 悪い例4:ファイル名を信用しすぎる
- 悪い例5:アップロード後の状態管理がない
- Laravelでのファイルアップロード例
- APIレスポンス例
- ファイルダウンロードAPIの設計
- REST APIでファイルアップロードを設計するときのチェックリスト
- まとめ
ファイルアップロードAPIは普通のJSON APIとは少し違う
通常のREST APIでは、JSONを使ってデータを送ることが多いです。
たとえば、記事を作成するAPIは次のようになります。
POST /articles
Content-Type: application/json
{
"title": "API設計の基本",
"body": "本文です。"
}
しかし、ファイルアップロードでは、画像やPDFなどのバイナリデータを送る必要があります。
そのため、JSONだけでは扱いにくいです。
ファイルアップロードでは、主に次の2つの方式が使われます。
| 方式 | 内容 |
|---|---|
| multipart/form-data | APIサーバーにファイルを送る |
| 署名付きURL方式 | S3などのストレージに直接アップロードする |
どちらが正解というより、ファイルサイズ・システム構成・セキュリティ・運用方針によって使い分けます。
方式1:multipart/form-dataでAPIサーバーにアップロードする
もっともシンプルなのは、multipart/form-data を使ってAPIサーバーにファイルを送る方式です。
たとえば、プロフィール画像をアップロードするAPIは次のように設計できます。
POST /users/1/avatar
Content-Type: multipart/form-data
送信するデータは次のようなイメージです。
file: avatar.png
JavaScriptで送る場合は、FormData を使います。
const formData = new FormData();
formData.append('file', file);
await fetch('/users/1/avatar', {
method: 'POST',
body: formData,
});
この方式では、クライアントがAPIサーバーにファイルを送り、APIサーバーがファイルを保存します。
クライアント
↓ ファイル送信
APIサーバー
↓ 保存
ストレージ・ファイルサーバー
multipart/form-data方式のメリット
multipart/form-data 方式のメリットは、実装がわかりやすいことです。
特に、次のようなケースでは使いやすいです。
- 小さな画像をアップロードする
- 管理画面からPDFを登録する
- CSVをアップロードして取り込む
- 最初のMVPとしてシンプルに作りたい
- APIサーバー側でバリデーションや加工をしたい
たとえば、プロフィール画像や商品画像のような小さめのファイルであれば、この方式で十分なことが多いです。
multipart/form-data方式のデメリット
一方で、デメリットもあります。
APIサーバーを経由してファイルを送るため、ファイルサイズが大きいとサーバーに負荷がかかります。
たとえば、動画や音声ファイルをアップロードする場合、APIサーバーが次のような負荷を受けます。
- 大きなリクエストを受け取る
- 一時ファイルを保持する
- ストレージへ転送する
- タイムアウト対策が必要になる
- アップロード中のメモリ・ディスク使用量が増える
小さな画像なら問題なくても、大きなファイルを大量に扱う場合は、APIサーバーを経由する設計がボトルネックになることがあります。
方式2:署名付きURLでストレージに直接アップロードする
もう一つよく使われるのが、署名付きURLを使ってS3などのストレージに直接アップロードする方式です。
署名付きURLとは、一定時間だけ有効なアップロード用URLです。
この方式では、まずAPIサーバーからアップロード用URLを発行します。
POST /uploads/presigned-url
Content-Type: application/json
{
"file_name": "avatar.png",
"content_type": "image/png"
}
レスポンス例は次のようになります。
{
"upload_url": "https://example-bucket.s3.ap-northeast-1.amazonaws.com/...",
"file_key": "users/1/avatar/avatar.png",
"expires_in": 300
}
クライアントは、この upload_url に対して直接ファイルをアップロードします。
クライアント
↓ 署名付きURL発行依頼
APIサーバー
↓ upload_urlを返す
クライアント
↓ 直接アップロード
S3などのストレージ
署名付きURL方式のメリット
署名付きURL方式の大きなメリットは、APIサーバーに大きなファイルを通さなくてよいことです。
ファイル本体はクライアントからストレージに直接アップロードされます。
そのため、次のようなケースに向いています。
- 動画ファイルをアップロードする
- 音声ファイルをアップロードする
- 大きなPDFを扱う
- アップロード数が多い
- APIサーバーの負荷を減らしたい
- S3などのオブジェクトストレージを使う
特に、ファイルサイズが大きくなる可能性がある場合は、署名付きURL方式を検討した方がよいです。
署名付きURL方式のデメリット
署名付きURL方式は便利ですが、設計は少し複雑になります。
たとえば、次のようなことを考える必要があります。
- アップロードURLの有効期限
- アップロード後にDBへどう登録するか
- アップロードに失敗した場合の扱い
- 未使用ファイルの削除
- Content-Typeの検証
- ファイルサイズ制限
- 誰がどのパスにアップロードできるか
- アップロード完了通知の設計
つまり、署名付きURL方式はスケールしやすい一方で、API設計としては少し難易度が上がります。
どちらの方式を選ぶべきか
ファイルアップロード方式は、次のように選ぶとわかりやすいです。
| 条件 | おすすめ |
|---|---|
| 小さな画像・PDFを扱う | multipart/form-data |
| 管理画面中心で利用する | multipart/form-data |
| MVPとして素早く作りたい | multipart/form-data |
| 大きな動画・音声を扱う | 署名付きURL |
| アップロード頻度が高い | 署名付きURL |
| APIサーバーの負荷を減らしたい | 署名付きURL |
| S3などに直接保存したい | 署名付きURL |
初心者向けに言うなら、まずは次の判断で十分です。
小さいファイルならAPIサーバー経由
大きいファイルならストレージ直接アップロード
ファイルアップロードAPIの基本設計
ここからは、REST APIとしてファイルアップロードをどう設計するかを具体的に見ていきます。
たとえば、ユーザーのプロフィール画像をアップロードする場合を考えます。
例1:プロフィール画像をアップロードするAPI
プロフィール画像は、ユーザーに紐づくファイルです。
この場合、エンドポイントは次のようにできます。
POST /users/{user_id}/avatar
リクエストは multipart/form-data です。
file: avatar.png
レスポンス例は次の通りです。
{
"id": 10,
"user_id": 1,
"file_name": "avatar.png",
"mime_type": "image/png",
"size": 123456,
"url": "https://example.com/storage/users/1/avatar.png"
}
このAPIは、「ユーザーのプロフィール画像を登録する」という意味です。
プロフィール画像は通常1つなので、既存の画像がある場合は差し替える設計にすることが多いです。
例2:記事に画像を添付するAPI
記事に複数の画像を添付できる場合は、次のように設計できます。
POST /articles/{article_id}/attachments
リクエストは次のようになります。
file: image.png
レスポンス例です。
{
"id": 101,
"article_id": 5,
"file_name": "image.png",
"mime_type": "image/png",
"size": 234567,
"url": "https://example.com/storage/articles/5/image.png",
"created_at": "2026-05-21T10:00:00+09:00"
}
このAPIでは、記事に対して添付ファイルを追加しています。
複数ファイルを扱うので、attachments のような複数形のリソースにするのが自然です。
例3:アップロード済みファイル一覧を取得するAPI
アップロードしたファイルを一覧表示したい場合は、GETを使います。
GET /articles/{article_id}/attachments
レスポンス例です。
{
"data": [
{
"id": 101,
"file_name": "image.png",
"mime_type": "image/png",
"size": 234567,
"url": "https://example.com/storage/articles/5/image.png"
},
{
"id": 102,
"file_name": "document.pdf",
"mime_type": "application/pdf",
"size": 345678,
"url": "https://example.com/storage/articles/5/document.pdf"
}
]
}
ファイルもAPI上ではリソースとして扱えます。
そのため、アップロードだけでなく、一覧取得・詳細取得・削除なども設計対象になります。
例4:ファイルを削除するAPI
添付ファイルを削除する場合は、DELETEを使います。
DELETE /attachments/{attachment_id}
レスポンスは、削除成功なら 204 No Content にすることが多いです。
204 No Content
または、削除結果をJSONで返してもよいです。
{
"message": "ファイルを削除しました"
}
ただし、API全体でどちらの形式にするかは統一した方がよいです。
例5:ファイルを差し替えるAPI
ファイルを差し替える場合は、少し設計に迷いやすいです。
たとえば、プロフィール画像を差し替えるなら、次のようにできます。
PUT /users/{user_id}/avatar
これは、「ユーザーのプロフィール画像というリソースを置き換える」という意味です。
一方で、単に新しい画像をアップロードして古い画像を無効化する設計なら、次のようにPOSTでも問題ありません。
POST /users/{user_id}/avatar
実務では、プロフィール画像のアップロードはPOSTで差し替えまで行うケースも多いです。
重要なのは、API仕様として次の点を明確にすることです。
- 既存ファイルがある場合に上書きするのか
- 新しいファイルとして追加するのか
- 古いファイルを削除するのか
- 古いファイルを履歴として残すのか
ファイル情報はDBに保存するべきか
ファイルアップロードでは、ファイル本体だけでなく、ファイルのメタ情報をDBに保存することが多いです。
たとえば、次のようなテーブルです。
attachments
| カラム | 内容 |
|---|---|
| id | ファイルID |
| attachable_type | 紐づくリソースの種類 |
| attachable_id | 紐づくリソースID |
| file_name | 元のファイル名 |
| file_path | 保存先パス |
| mime_type | MIMEタイプ |
| size | ファイルサイズ |
| uploaded_by | アップロードしたユーザーID |
| created_at | 作成日時 |
| updated_at | 更新日時 |
ファイル本体はS3やローカルストレージに保存し、DBには参照情報を保存するイメージです。
DB
- file_name
- file_path
- mime_type
- size
Storage
- 実際のファイル本体
DBに保存しておきたい情報
ファイル情報として、最低限次のような情報を保存しておくと扱いやすいです。
| 項目 | 理由 |
|---|---|
| 元ファイル名 | 画面表示やダウンロード名に使える |
| 保存パス | ストレージ上の場所を特定する |
| MIMEタイプ | 画像・PDFなどの判定に使える |
| ファイルサイズ | 制限や表示に使える |
| アップロード者 | 権限管理や監査に使える |
| 関連リソースID | 記事・ユーザーなどと紐づける |
| 作成日時 | 並び替えや履歴に使える |
逆に、ファイルの公開URLだけをDBに保存する設計は注意が必要です。
ストレージ構成やドメインが変わったときに扱いづらくなるからです。
おすすめは、URLそのものではなく、ストレージ内のキーやパスを保存することです。
{
"file_path": "articles/5/attachments/image.png"
}
必要に応じて、API側でURLを生成して返します。
ファイルURLをそのまま返してよいか
ファイルURLをレスポンスで返すかどうかは、ファイルの公開範囲によって変わります。
公開ファイルの場合
ブログのアイキャッチ画像や商品画像のように、誰でも見てよいファイルなら、公開URLを返してもよいです。
{
"url": "https://cdn.example.com/articles/5/image.png"
}
この場合、CDNを使って配信することも多いです。
非公開ファイルの場合
契約書、診療データ、音声ファイル、個人情報を含む画像などは、公開URLをそのまま返すべきではありません。
この場合は、認証済みユーザーだけがアクセスできるようにします。
方法としては、次のようなものがあります。
| 方法 | 内容 |
|---|---|
| API経由でダウンロード | APIサーバーが認可してファイルを返す |
| 署名付きURLで一時的に閲覧 | 短時間だけ有効なURLを発行する |
| CDN + 認証制御 | Cookieや署名付きURLで制限する |
たとえば、署名付きダウンロードURLを発行するAPIは次のように設計できます。
POST /attachments/{attachment_id}/download-url
レスポンス例です。
{
"download_url": "https://example-bucket.s3.ap-northeast-1.amazonaws.com/...",
"expires_in": 300
}
非公開ファイルでは、「URLを知っていれば誰でも見られる」状態にしないことが重要です。
ファイルアップロードAPIのセキュリティ対策
ファイルアップロードは、セキュリティ上かなり注意が必要です。
なぜなら、ユーザーが任意のファイルをサーバーに送れるからです。
最低限、次の点は考慮しましょう。
1. ファイルサイズ制限をかける
ファイルサイズ制限は必須です。
制限がないと、巨大なファイルをアップロードされて、サーバーやストレージに負荷がかかります。
たとえば、プロフィール画像なら次のように制限できます。
最大サイズ: 5MB
許可形式: jpg, png, webp
APIのバリデーションエラー例です。
422 Unprocessable Entity
{
"message": "アップロードできるファイルサイズを超えています",
"errors": {
"file": ["ファイルサイズは5MB以下にしてください"]
}
}
2. MIMEタイプを検証する
拡張子だけでファイル種別を判断するのは危険です。
たとえば、image.png という名前でも、実際には別のファイルである可能性があります。
そのため、次の両方を確認するのが望ましいです。
- 拡張子
- MIMEタイプ
- 可能であればファイルの中身
画像アップロードなら、許可する形式を限定します。
image/jpeg
image/png
image/webp
PDFなら次のようになります。
application/pdf
3. 実行可能ファイルをアップロードさせない
ユーザーがアップロードしたファイルを、そのままWebサーバー上で公開するのは危険です。
特に、次のようなファイルは注意が必要です。
.php.js.html.exe.sh.bat
たとえば、PHPが実行されるディレクトリに .php ファイルをアップロードできてしまうと、重大な脆弱性になります。
アップロードファイルは、基本的にアプリケーションコードとは別の安全なストレージに保存しましょう。
4. ファイル名をそのまま保存パスに使わない
ユーザーが送ってきたファイル名を、そのまま保存パスに使うのは避けた方がよいです。
たとえば、次のようなファイル名が送られる可能性があります。
../../../danger.php
また、日本語やスペース、特殊文字を含むファイル名も扱いにくいことがあります。
保存時は、UUIDなどを使って安全なファイル名に変換するのがおすすめです。
uploads/users/1/avatar/8f3a9c2e4b6d.png
元のファイル名は、表示用としてDBに保存します。
{
"original_file_name": "プロフィール画像.png",
"file_path": "uploads/users/1/avatar/8f3a9c2e4b6d.png"
}
5. 認可チェックを必ず行う
ファイルアップロードでは、認証だけでなく認可も重要です。
たとえば、次のAPIを考えます。
POST /users/1/avatar
ログインしていれば誰でもユーザー1の画像を変更できる、という設計は危険です。
サーバー側で、現在のログインユーザーがそのファイルをアップロードしてよいか確認する必要があります。
ログインユーザーID = 1 → OK
ログインユーザーID = 2 → NG
エラー例です。
403 Forbidden
{
"message": "このユーザーの画像を変更する権限がありません"
}
6. ウイルススキャンを検討する
業務システムや外部公開サービスでファイルアップロードを受け付ける場合、ウイルススキャンも検討するべきです。
特に、次のようなファイルを扱う場合は重要です。
- Word
- Excel
- ZIP
- 取引先から受け取るファイル
- 不特定多数がアップロードするファイル
ファイルアップロード直後は一時的に pending 状態にして、スキャン完了後に利用可能にする設計もあります。
{
"id": 101,
"status": "scanning"
}
スキャン完了後に次のように更新します。
{
"id": 101,
"status": "available"
}
アップロード後に非同期処理が必要な場合
ファイルアップロード後に、追加処理が必要になることがあります。
たとえば、次のような処理です。
- 画像のリサイズ
- サムネイル生成
- 動画のエンコード
- 音声の文字起こし
- CSVの取り込み
- ウイルススキャン
- PDFの解析
このような処理は、アップロードAPIの中で同期的に実行しない方がよいことがあります。
処理に時間がかかると、APIレスポンスが遅くなったり、タイムアウトしたりするからです。
その場合は、アップロード時点では受付だけ行い、バックグラウンドジョブで処理します。
POST /files
レスポンス例です。
202 Accepted
{
"id": 101,
"status": "processing",
"message": "ファイルを受け付けました。処理完了までお待ちください。"
}
処理状況を確認するAPIは次のようにできます。
GET /files/101
レスポンス例です。
{
"id": 101,
"status": "completed",
"file_name": "sample.csv",
"result": {
"imported_count": 100,
"failed_count": 2
}
}
時間がかかる処理では、202 Accepted とステータス管理を使う設計が有効です。
署名付きURL方式のAPI設計例
ここからは、S3などに直接アップロードする方式の設計例を見ていきます。
1. アップロードURLを発行する
まず、クライアントはAPIサーバーにアップロードURLの発行を依頼します。
POST /upload-urls
Content-Type: application/json
{
"file_name": "voice.m4a",
"content_type": "audio/mp4",
"size": 1234567
}
サーバーは、認証・認可・ファイルサイズ・MIMEタイプをチェックしたうえで、署名付きURLを発行します。
レスポンス例です。
{
"upload_url": "https://example-bucket.s3.ap-northeast-1.amazonaws.com/...",
"file_key": "users/1/uploads/9f3a2c1b.m4a",
"expires_in": 300
}
2. クライアントがストレージへ直接アップロードする
クライアントは、受け取った upload_url に対してファイルをPUTします。
PUT https://example-bucket.s3.ap-northeast-1.amazonaws.com/...
Content-Type: audio/mp4
この通信はAPIサーバーではなく、ストレージに対して行います。
3. アップロード完了をAPIに通知する
アップロードが完了したら、クライアントはAPIサーバーに完了通知を送ります。
POST /files
Content-Type: application/json
{
"file_key": "users/1/uploads/9f3a2c1b.m4a",
"file_name": "voice.m4a",
"content_type": "audio/mp4",
"size": 1234567
}
サーバーは、ファイル情報をDBに保存します。
レスポンス例です。
{
"id": 101,
"file_name": "voice.m4a",
"mime_type": "audio/mp4",
"size": 1234567,
"status": "uploaded"
}
このように、署名付きURL方式では、ファイル本体のアップロードとDB登録を分けて考えます。
署名付きURL方式で注意すべきこと
署名付きURL方式では、アップロードURLを発行しただけで、実際にアップロードされたとは限りません。
次のような状態が起こります。
1. upload_urlを発行した
2. しかしクライアントがアップロードしなかった
3. ストレージにファイルが存在しない
または、逆に次のような状態もあります。
1. ストレージにはアップロードされた
2. しかしDB登録APIが呼ばれなかった
3. DBにファイル情報が存在しない
そのため、未完了のアップロードや孤児ファイルをどう扱うかを考える必要があります。
対策としては、次のようなものがあります。
- アップロード予約テーブルを作る
- 一定時間経過した未完了アップロードを削除する
- ストレージ上の孤児ファイルを定期削除する
- アップロード完了後にサーバー側で存在確認する
- ファイルキーにユーザーIDや用途を含める
署名付きURL方式は便利ですが、ファイル管理の整合性を意識する必要があります。
ファイルアップロードのエンドポイント設計例
ファイルを独立したリソースとして扱う場合、次のようなAPI設計にできます。
POST /files
GET /files/{file_id}
DELETE /files/{file_id}
特定のリソースに紐づける場合は、次のようにできます。
POST /articles/{article_id}/attachments
GET /articles/{article_id}/attachments
DELETE /attachments/{attachment_id}
署名付きURLを使う場合は、次のような設計もあります。
POST /upload-urls
POST /files
GET /files/{file_id}
POST /files/{file_id}/download-url
どれが正解というより、ファイルが何に紐づくのかを考えることが大切です。
ファイルをリソースとして考える
REST APIでは、ファイルもリソースとして考えられます。
たとえば、次のようなものです。
| ファイルの種類 | リソース名の例 |
|---|---|
| 記事の添付ファイル | attachments |
| ユーザー画像 | avatar |
| 商品画像 | product_images |
| アップロードファイル全般 | files |
| 音声録音 | recordings |
| 請求書PDF | invoices/{id}/pdf |
たとえば、音声録音ファイルなら次のようなAPIが考えられます。
POST /recordings
GET /recordings/{recording_id}
DELETE /recordings/{recording_id}
記事の添付ファイルなら次のようになります。
POST /articles/{article_id}/attachments
GET /articles/{article_id}/attachments
DELETE /attachments/{attachment_id}
リソースとして考えると、アップロード後の取得・削除・権限管理も整理しやすくなります。
ファイルアップロードで使うHTTPメソッド
ファイルアップロードでも、HTTPメソッドの考え方は通常のREST APIと同じです。
| 操作 | メソッド | 例 |
|---|---|---|
| ファイルを追加する | POST | POST /attachments |
| ファイル情報を取得する | GET | GET /attachments/1 |
| ファイルを削除する | DELETE | DELETE /attachments/1 |
| ファイルを差し替える | PUT | PUT /users/1/avatar |
| ファイルのメタ情報を変更する | PATCH | PATCH /attachments/1 |
たとえば、ファイル名だけを変更したい場合はPATCHが自然です。
PATCH /attachments/101
Content-Type: application/json
{
"display_name": "契約書.pdf"
}
ファイル本体の差し替えなら、PUTまたはPOSTを検討します。
multipart/form-dataでメタ情報も一緒に送る場合
ファイルと一緒にメタ情報を送りたいこともあります。
たとえば、記事に添付ファイルを追加するとき、説明文も一緒に送りたい場合です。
POST /articles/5/attachments
Content-Type: multipart/form-data
file: document.pdf
description: 補足資料です
category: reference
JavaScriptでは次のようになります。
const formData = new FormData();
formData.append('file', file);
formData.append('description', '補足資料です');
formData.append('category', 'reference');
await fetch('/articles/5/attachments', {
method: 'POST',
body: formData,
});
この設計はシンプルで扱いやすいです。
ただし、複雑なJSON構造を一緒に送りたい場合は、multipartの中にJSON文字列を入れることもあります。
file: document.pdf
metadata: {"description":"補足資料です","category":"reference"}
この場合、サーバー側で metadata をJSONとしてパースします。
複数ファイルアップロードの設計
複数ファイルを一度にアップロードしたい場合もあります。
たとえば、記事に複数画像を添付するAPIです。
POST /articles/5/attachments
Content-Type: multipart/form-data
files[]: image1.png
files[]: image2.png
files[]: image3.png
レスポンス例です。
{
"data": [
{
"id": 101,
"file_name": "image1.png",
"url": "https://example.com/storage/image1.png"
},
{
"id": 102,
"file_name": "image2.png",
"url": "https://example.com/storage/image2.png"
},
{
"id": 103,
"file_name": "image3.png",
"url": "https://example.com/storage/image3.png"
}
]
}
ただし、複数ファイルアップロードでは、エラー時の扱いを決めておく必要があります。
たとえば、3ファイル中1ファイルだけ失敗した場合です。
設計方針は主に2つあります。
| 方針 | 内容 |
|---|---|
| 全部成功か全部失敗 | 1つでも失敗したら全体を失敗にする |
| 部分成功を許可 | 成功したファイルだけ保存する |
部分成功を許可する場合、レスポンスで成功・失敗を分けて返す必要があります。
{
"uploaded": [
{
"id": 101,
"file_name": "image1.png"
}
],
"failed": [
{
"file_name": "image2.png",
"reason": "ファイルサイズが上限を超えています"
}
]
}
シンプルにしたいなら、最初は1リクエスト1ファイルにする設計も有効です。
ファイルアップロードのステータスコード
ファイルアップロードAPIでは、次のステータスコードをよく使います。
| ステータスコード | 用途 |
|---|---|
| 201 Created | ファイル作成・登録成功 |
| 202 Accepted | 非同期処理を受け付けた |
| 204 No Content | 削除成功 |
| 400 Bad Request | リクエスト形式が不正 |
| 401 Unauthorized | 未認証 |
| 403 Forbidden | 権限なし |
| 404 Not Found | 対象リソースが存在しない |
| 413 Payload Too Large | ファイルサイズが大きすぎる |
| 415 Unsupported Media Type | 対応していないファイル形式 |
| 422 Unprocessable Entity | バリデーションエラー |
| 500 Internal Server Error | サーバーエラー |
たとえば、対応していないファイル形式なら次のように返せます。
415 Unsupported Media Type
{
"message": "このファイル形式はアップロードできません"
}
ファイルサイズが大きすぎる場合は、次のようになります。
413 Payload Too Large
{
"message": "ファイルサイズが上限を超えています"
}
ただし、実務ではバリデーションエラーとして 422 にまとめることもあります。
大切なのは、プロジェクト内で一貫していることです。
ファイルアップロードでよくある悪い設計
ここからは、避けた方がよい設計を見ていきます。
悪い例1:Base64でJSONに埋め込む
ファイルをBase64文字列に変換してJSONで送る設計があります。
{
"file_name": "avatar.png",
"file_data": "iVBORw0KGgoAAAANSUhEUgAA..."
}
これは小さなファイルなら動きます。
しかし、基本的にはおすすめしません。
理由は次の通りです。
- データサイズが増える
- メモリ使用量が増える
- 大きなファイルに向かない
- 通信効率が悪い
- 通常のファイルアップロード処理と相性が悪い
特別な理由がない限り、ファイル本体は multipart/form-data またはストレージ直接アップロードで扱う方が自然です。
悪い例2:ファイルをDBに直接保存する
ファイル本体をDBのBLOBカラムに保存する設計もあります。
小規模な用途では成立することもありますが、一般的なWebアプリでは慎重に考えるべきです。
理由は次の通りです。
- DBサイズが大きくなる
- バックアップが重くなる
- パフォーマンスに影響しやすい
- CDN配信しにくい
- ストレージ管理が複雑になる
多くの場合、ファイル本体はS3などのストレージに保存し、DBにはパスやメタ情報を保存する方が扱いやすいです。
悪い例3:公開してはいけないファイルを公開URLで返す
非公開ファイルなのに、誰でもアクセスできるURLを返してしまうのは危険です。
{
"url": "https://example.com/uploads/private/contract.pdf"
}
このURLを知っていれば誰でも見られる状態だと、情報漏えいにつながります。
非公開ファイルでは、認可チェックを通した上で、一時的なダウンロードURLを発行するなどの対策が必要です。
悪い例4:ファイル名を信用しすぎる
ユーザーが送ってきたファイル名を信用しすぎるのも危険です。
../../../../etc/passwd
malware.php
image.png.exe
ファイル名は表示用として扱い、保存パスには安全なランダム文字列やUUIDを使うのが基本です。
悪い例5:アップロード後の状態管理がない
アップロード後に処理が必要なのに、状態管理をしない設計も問題です。
たとえば、動画エンコードやウイルススキャンが必要な場合、ファイルはすぐに使えるとは限りません。
その場合は、次のようなステータスを持たせるとよいです。
uploaded
processing
available
failed
deleted
レスポンス例です。
{
"id": 101,
"status": "processing"
}
ファイルを単なるURLとして扱うのではなく、状態を持つリソースとして設計すると管理しやすくなります。
Laravelでのファイルアップロード例
Laravelで multipart/form-data のファイルアップロードを受け取る場合、次のように実装できます。
public function store(Request $request)
{
$validated = $request->validate([
'file' => ['required', 'file', 'mimes:jpg,png,webp,pdf', 'max:5120'],
]);
$file = $request->file('file');
$path = $file->store('uploads', 's3');
$attachment = Attachment::create([
'original_file_name' => $file->getClientOriginalName(),
'file_path' => $path,
'mime_type' => $file->getMimeType(),
'size' => $file->getSize(),
'uploaded_by' => $request->user()->id,
]);
return response()->json($attachment, 201);
}
ポイントは、次の通りです。
fileバリデーションを行う- MIMEタイプや拡張子を制限する
- サイズ制限を行う
- 元ファイル名と保存パスを分ける
- アップロード者を保存する
- レスポンスでは作成済みリソースを返す
APIレスポンス例
ファイルアップロード成功時のレスポンスは、次のように設計できます。
{
"id": 101,
"original_file_name": "プロフィール画像.png",
"mime_type": "image/png",
"size": 123456,
"url": "https://cdn.example.com/uploads/users/1/avatar.png",
"created_at": "2026-05-21T10:00:00+09:00"
}
url を返すかどうかは、公開ファイルか非公開ファイルかで変わります。
非公開ファイルの場合は、URLを直接返さず、ダウンロード用APIを別に用意する方が安全です。
ファイルダウンロードAPIの設計
アップロードだけでなく、ダウンロードも設計しておく必要があります。
公開ファイルなら、CDNや公開URLで直接配信できます。
非公開ファイルなら、次のようなAPIを用意します。
GET /attachments/{attachment_id}/download
このAPIでは、サーバー側で次を確認します。
- ログインしているか
- そのファイルを見る権限があるか
- ファイルが存在するか
- ファイルが削除済みではないか
問題なければ、ファイルを返すか、署名付きURLにリダイレクトします。
または、署名付きURLをJSONで返す設計もあります。
POST /attachments/{attachment_id}/download-url
{
"download_url": "https://example-bucket.s3.ap-northeast-1.amazonaws.com/...",
"expires_in": 300
}
REST APIでファイルアップロードを設計するときのチェックリスト
最後に、設計時のチェックリストをまとめます。
アップロード方式
multipart/form-dataで十分か- 署名付きURL方式が必要か
- ファイルサイズはどれくらいか
- アップロード頻度はどれくらいか
- APIサーバーを経由して問題ないか
エンドポイント設計
- ファイルをどのリソースに紐づけるか
- 単一ファイルか複数ファイルか
- 追加なのか差し替えなのか
- 削除APIは必要か
- ダウンロードAPIは必要か
セキュリティ
- 認証が必要か
- 認可チェックをしているか
- ファイルサイズ制限があるか
- MIMEタイプを制限しているか
- 危険な拡張子を拒否しているか
- 非公開ファイルを公開URLで返していないか
- ファイル名をそのまま保存パスに使っていないか
保存設計
- ファイル本体はどこに保存するか
- DBにどのメタ情報を保存するか
- ストレージパスをどう設計するか
- 元ファイル名と保存ファイル名を分けているか
- 削除時にストレージ上のファイルも消すか
運用
- 未使用ファイルを削除する仕組みがあるか
- 非同期処理が必要か
- 処理ステータスを持つべきか
- ログや監査情報を残すか
- ストレージ容量の増加を監視するか
まとめ
REST APIでファイルアップロードを設計するときは、単に「ファイルを送れるAPI」を作るだけでは不十分です。
次のような観点を含めて設計する必要があります。
| 観点 | 考えること |
|---|---|
| アップロード方式 | APIサーバー経由か、ストレージ直接アップロードか |
| エンドポイント | どのリソースにファイルを紐づけるか |
| 保存先 | ローカル、S3、その他ストレージ |
| DB設計 | ファイル名、パス、MIMEタイプ、サイズなど |
| セキュリティ | サイズ制限、形式制限、認可、非公開制御 |
| レスポンス | URLを返すか、メタ情報だけ返すか |
| 非同期処理 | 画像変換、スキャン、CSV取込など |
| 運用 | 未使用ファイル削除、監視、ログ管理 |
基本的な判断としては、次のように考えるとわかりやすいです。
小さなファイルは
multipart/form-dataでAPIサーバーに送る
大きなファイルや大量アップロードは署名付きURLでストレージに直接送る
ファイル本体はストレージに保存し、DBにはメタ情報を保存する
非公開ファイルはURLをそのまま公開せず、認可チェックや署名付きURLを使う
ファイルアップロードは、API設計・ストレージ設計・セキュリティ設計が絡む領域です。
最初はシンプルな設計で始めても構いませんが、ファイルサイズ、公開範囲、アップロード頻度、将来の運用を考えながら設計することが大切です。

大阪のエンジニアが書いているブログ。



コメント