REST APIでファイルアップロードはどう設計するべきか

ITインフラ
この記事は約29分で読めます。

この記事の最終更新日: 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でファイルアップロードを設計するときの考え方を、具体例を使って解説します。


  1. ファイルアップロードAPIは普通のJSON APIとは少し違う
  2. 方式1:multipart/form-dataでAPIサーバーにアップロードする
  3. multipart/form-data方式のメリット
  4. multipart/form-data方式のデメリット
  5. 方式2:署名付きURLでストレージに直接アップロードする
  6. 署名付きURL方式のメリット
  7. 署名付きURL方式のデメリット
  8. どちらの方式を選ぶべきか
  9. ファイルアップロードAPIの基本設計
  10. 例1:プロフィール画像をアップロードするAPI
  11. 例2:記事に画像を添付するAPI
  12. 例3:アップロード済みファイル一覧を取得するAPI
  13. 例4:ファイルを削除するAPI
  14. 例5:ファイルを差し替えるAPI
  15. ファイル情報はDBに保存するべきか
  16. DBに保存しておきたい情報
  17. ファイルURLをそのまま返してよいか
    1. 公開ファイルの場合
    2. 非公開ファイルの場合
  18. ファイルアップロードAPIのセキュリティ対策
  19. 1. ファイルサイズ制限をかける
  20. 2. MIMEタイプを検証する
  21. 3. 実行可能ファイルをアップロードさせない
  22. 4. ファイル名をそのまま保存パスに使わない
  23. 5. 認可チェックを必ず行う
  24. 6. ウイルススキャンを検討する
  25. アップロード後に非同期処理が必要な場合
  26. 署名付きURL方式のAPI設計例
  27. 1. アップロードURLを発行する
  28. 2. クライアントがストレージへ直接アップロードする
  29. 3. アップロード完了をAPIに通知する
  30. 署名付きURL方式で注意すべきこと
  31. ファイルアップロードのエンドポイント設計例
  32. ファイルをリソースとして考える
  33. ファイルアップロードで使うHTTPメソッド
  34. multipart/form-dataでメタ情報も一緒に送る場合
  35. 複数ファイルアップロードの設計
  36. ファイルアップロードのステータスコード
  37. ファイルアップロードでよくある悪い設計
  38. 悪い例1:Base64でJSONに埋め込む
  39. 悪い例2:ファイルをDBに直接保存する
  40. 悪い例3:公開してはいけないファイルを公開URLで返す
  41. 悪い例4:ファイル名を信用しすぎる
  42. 悪い例5:アップロード後の状態管理がない
  43. Laravelでのファイルアップロード例
  44. APIレスポンス例
  45. ファイルダウンロードAPIの設計
  46. REST APIでファイルアップロードを設計するときのチェックリスト
    1. アップロード方式
    2. エンドポイント設計
    3. セキュリティ
    4. 保存設計
    5. 運用
  47. まとめ

ファイルアップロードAPIは普通のJSON APIとは少し違う

通常のREST APIでは、JSONを使ってデータを送ることが多いです。

たとえば、記事を作成するAPIは次のようになります。

POST /articles
Content-Type: application/json

{
  "title": "API設計の基本",
  "body": "本文です。"
}

しかし、ファイルアップロードでは、画像やPDFなどのバイナリデータを送る必要があります。

そのため、JSONだけでは扱いにくいです。

ファイルアップロードでは、主に次の2つの方式が使われます。

方式内容
multipart/form-dataAPIサーバーにファイルを送る
署名付き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_typeMIMEタイプ
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. ウイルススキャンを検討する

業務システムや外部公開サービスでファイルアップロードを受け付ける場合、ウイルススキャンも検討するべきです。

特に、次のようなファイルを扱う場合は重要です。

  • PDF
  • 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
請求書PDFinvoices/{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と同じです。

操作メソッド
ファイルを追加するPOSTPOST /attachments
ファイル情報を取得するGETGET /attachments/1
ファイルを削除するDELETEDELETE /attachments/1
ファイルを差し替えるPUTPUT /users/1/avatar
ファイルのメタ情報を変更するPATCHPATCH /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設計・ストレージ設計・セキュリティ設計が絡む領域です。

最初はシンプルな設計で始めても構いませんが、ファイルサイズ、公開範囲、アップロード頻度、将来の運用を考えながら設計することが大切です。

コメント

タイトルとURLをコピーしました