テックブログ

multipart/form-data入門:ファイルアップロードAPIの基本

multipart/form-data入門:ファイルアップロードAPIの基本

multipart/form-dataは、フォームのテキスト項目や画像・PDFなどのファイルを、複数のパートに分けてHTTPリクエストで送るための形式です。ブラウザではFormDataを使うことで、ファイルと通常の入力値をまとめて扱いやすくなります。ただし、Content-Typeの自動設定、ファイルサイズ制限、MIMEタイプ確認、保存先や公開範囲の設計を誤ると、アップロード失敗やセキュリティ上の問題につながります。

1. multipart/form-dataとは何か

1-1. 複数のデータをパートに分けて送る形式

multipart/form-dataは、1つのHTTPリクエストの中で、複数のデータをパートに分けて送る形式です。テキスト項目とファイルを同時に送るときによく使われます。

通常のフォームでは、名前、説明文、カテゴリなどのテキスト項目だけでなく、画像やPDFなどのファイルを一緒に送りたい場面があります。multipart/form-dataでは、それぞれの項目を区切りながら送れるため、サーバー側は「これはtitle」「これはfile」のように分けて受け取れます。この区切りに使われる情報がboundaryです。

たとえば、プロフィール編集で「表示名」と「アイコン画像」を同時に送る場合、multipart/form-dataが向いています。JSONのように1つの文字列としてまとめるのではなく、フォーム項目ごとに分けて送る形式だと理解すると分かりやすいです。

1-2. JSON送信との違い

multipart/form-dataとJSON送信の違いは、ファイルを自然に扱えるかどうかです。JSONはテキストデータの送受信に向いていますが、ファイルそのものを送る形式としては扱いにくいです。

JSONでは、オブジェクトや配列を文字列として表現します。ユーザー名やメールアドレス、設定値のようなデータには向いています。一方、画像やPDFなどのファイルはバイナリデータなので、JSONにそのまま入れることはできません。Base64へ変換する方法もありますが、データ量が増えたり、処理が複雑になったりします。

たとえば、ユーザー登録情報だけを送るなら application/json が自然です。一方で、商品名、説明文、商品画像を同時に送るなら multipart/form-data が向いています。送る内容がテキスト中心なのか、ファイルを含むのかで使い分けましょう。

1-3. ファイルアップロードで使われる理由

ファイルアップロードでmultipart/form-dataが使われる理由は、ファイルと通常の入力項目を1つのリクエストでまとめて送れるからです。Webフォームとの相性もよく、ブラウザ標準の仕組みとして扱いやすいです。

ファイルアップロードでは、ファイル本体だけでなく、タイトル、説明文、種類、関連IDなども一緒に送りたいことがあります。multipart/form-dataなら、ファイル用のパートとテキスト用のパートを分けて送れるため、サーバー側でも整理して受け取れます。ブラウザでは FormData を使うことで、この形式を比較的簡単に作れます。

ただし、multipart/form-dataを使えば安全にアップロードできるわけではありません。ファイルサイズ、MIMEタイプ、拡張子、保存先、公開範囲の確認が必要です。ファイルアップロードは外部からデータを受け取る入口なので、入力チェックと同じく慎重に設計する必要があります。

2. FormDataの基本

2-1. ブラウザでFormDataを作る

ブラウザでmultipart/form-dataを送るときは、FormDataを使うのが基本です。FormDataは、フォームの入力値やファイルをまとめてリクエスト本文に入れるためのWeb APIです。

FormDataを使うと、テキスト項目もファイル項目も同じように append で追加できます。fetchやaxiosのbodyにFormDataを渡すと、ブラウザがmultipart/form-data形式の本文を作ります。つまり、開発者がboundaryを自分で組み立てる必要はありません。

const formData = new FormData();

formData.append('title', 'プロフィール画像');
formData.append('description', 'ユーザーのアイコン画像です');

このコードでは、FormDataにテキスト項目を追加しています。注意点は、FormDataの中身は通常のJSONオブジェクトとは扱い方が違うことです。JSON.stringify(formData) のようにJSON化して送るのではなく、FormDataのままリクエスト本文へ渡します。

2-2. テキスト項目とファイルを追加する

FormDataでは、テキスト項目とファイルを同じリクエストに追加できます。画像アップロードや添付ファイル送信では、この使い方が基本になります。

HTMLの <input type="file"> から取得したファイルは、File オブジェクトとして扱えます。それをFormDataへ追加すると、ブラウザがファイル用のパートとして送信します。テキスト項目も一緒に追加できるため、「画像ファイル」と「画像タイトル」を同時にAPIへ送れます。

const fileInput = document.querySelector('#avatar');

const formData = new FormData();
formData.append('displayName', '山田太郎');
formData.append('avatar', fileInput.files[0]);

fetch('/api/profile/avatar', {
  method: 'POST',
  body: formData
});

このコードでは、表示名と画像ファイルを同じリクエストで送っています。注意点は、fileInput.files[0] が存在するかを送信前に確認することです。ファイル未選択のまま送ると、サーバー側で期待したファイルが受け取れない原因になります。

2-3. Content-Typeを手動設定しない方がよい理由

FormDataをfetchで送る場合、Content-Typeを手動で固定しない方が安全です。ブラウザがboundaryを含む正しいContent-Typeを自動で設定してくれるからです。

multipart/form-dataでは、各パートを区切るためのboundaryが必要です。たとえばContent-Typeには multipart/form-data; boundary=----xxxx のような情報が含まれます。開発者が Content-Type: multipart/form-data だけを手動指定すると、boundaryが不足し、サーバー側で正しく解析できないことがあります。

// 避けたい例
fetch('/api/upload', {
  method: 'POST',
  headers: {
    'Content-Type': 'multipart/form-data'
  },
  body: formData
});

このコードでは、Content-Typeを手動指定しているため、boundaryが正しく付かない可能性があります。FormDataを使う場合は、基本的に headers のContent-Typeを省き、ブラウザやHTTPクライアントに任せましょう。

3. API側で受け取るときの考え方

3-1. ファイルと通常項目を分けて受け取る

API側では、ファイルと通常のフォーム項目を分けて受け取ることが大切です。multipart/form-dataでは、テキスト項目とファイル項目が別の性質を持つからです。

通常項目は文字列として受け取り、ファイルはファイル名、MIMEタイプ、サイズ、一時保存先、バッファなどの情報を持つデータとして受け取ります。サーバー側のフレームワークでは、multipart/form-dataを解析するためのミドルウェアやライブラリが必要になることがあります。JSON用のパーサーだけではファイルを正しく処理できません。

// Express + multerの例
app.post('/api/upload', upload.single('avatar'), (req, res) => {
  const displayName = req.body.displayName;
  const file = req.file;

  res.json({
    displayName,
    fileName: file.originalname
  });
});

このコードでは、displayName を通常項目として、avatar をファイルとして受け取っています。注意点は、サーバー側の受け取り名とフロント側の formData.append('avatar', file) のキー名を一致させることです。名前がずれると、ファイルが届いていないように見えます。

3-2. ファイル名、MIMEタイプ、サイズを確認する

ファイルアップロードAPIでは、ファイル名、MIMEタイプ、サイズを必ず確認する必要があります。外部から送られるファイルをそのまま信用して保存するのは危険です。

ファイル名には予期しない文字やパス風の文字列が含まれる可能性があります。MIMEタイプは、画像なのかPDFなのかなどを判断する材料になりますが、これだけを完全に信用するのも危険です。サイズ制限がなければ、大きなファイルを送られてサーバーのメモリやディスクを圧迫する可能性があります。

const allowedMimeTypes = ['image/jpeg', 'image/png'];
const maxSize = 5 * 1024 * 1024; // 5MB

if (!allowedMimeTypes.includes(file.mimetype)) {
  return res.status(400).json({
    errorCode: 'INVALID_FILE_TYPE',
    message: 'JPEGまたはPNG画像をアップロードしてください。'
  });
}

if (file.size > maxSize) {
  return res.status(400).json({
    errorCode: 'FILE_TOO_LARGE',
    message: 'ファイルサイズは5MB以下にしてください。'
  });
}

このコードでは、MIMEタイプとファイルサイズを確認しています。注意点は、MIMEタイプや拡張子だけで完全に安全と判断しないことです。必要に応じてファイル内容の検査、画像の再エンコード、ウイルススキャンなども検討します。

3-3. エラー時のレスポンス設計

ファイルアップロードAPIでは、エラー理由をフロントが表示しやすい形で返すことが重要です。失敗理由が分からないと、ユーザーは何を直せばよいか判断できません。

アップロードでは、ファイル未選択、サイズ超過、形式不正、保存失敗、認証切れなど、さまざまな失敗が起きます。これらをすべて「アップロードに失敗しました」だけで返すと、フロント側でも適切なメッセージを出せません。errorCode とユーザー向け message を分けると扱いやすくなります。

{
  "errorCode": "FILE_TOO_LARGE",
  "message": "ファイルサイズは5MB以下にしてください。"
}

このJSON例では、フロント側が分岐しやすいエラーコードと、ユーザーに表示しやすいメッセージを返しています。注意点は、内部の保存パスや例外メッセージをそのまま返さないことです。利用者に必要な情報と、サーバーログに残す詳細情報は分けて扱いましょう。

4. ファイル保存の基本

4-1. ローカル保存、クラウドストレージ、DB保存の違い

アップロードされたファイルの保存先は、ローカルディスク、クラウドストレージ、DB保存の特徴を理解して選ぶ必要があります。どれか1つが常に正解ではありません。

ローカル保存は小規模な開発や検証では分かりやすいですが、サーバーを複数台にしたり、コンテナを再作成したりすると扱いづらくなります。クラウドストレージは、画像やPDFなどの保存に向いており、公開範囲や署名付きURLも設計しやすいです。DB保存はメタ情報の管理には向きますが、大きなファイル本体を入れるとDB容量やバックアップに影響することがあります。

実務では、ファイル本体はストレージ、DBにはファイルID、保存パス、MIMEタイプ、サイズ、所有者IDなどのメタ情報を保存する構成がよく使われます。初心者の段階では、「ファイル本体」と「ファイルに関する情報」を分けて考えると整理しやすいです。

4-2. 元ファイル名をそのまま使わない理由

アップロードされた元ファイル名は、保存名としてそのまま使わないのが基本です。衝突や文字化け、意図しないパス表現、情報漏えいの原因になることがあるからです。

ユーザーがアップロードするファイル名は自由に付けられます。同じ名前のファイルが複数届くこともありますし、日本語や記号を含んで環境によって扱いづらいこともあります。また、元ファイル名に個人名や社内情報が含まれている場合、そのまま公開URLに出ると不要な情報公開につながります。

const fileId = crypto.randomUUID();
const extension = '.png';
const saveName = `${fileId}${extension}`;

このコードでは、保存名にランダムなIDを使っています。注意点は、拡張子もユーザー入力をそのまま信用しないことです。MIMEタイプやファイル内容の確認結果に基づいて、安全な保存名を決めるほうがよいです。

4-3. 保存先URLと公開範囲の考え方

ファイルを保存するときは、そのファイルを誰が見られるべきかを先に決めることが重要です。保存できることと、公開してよいことは別です。

プロフィール画像のように公開してよいファイルもあれば、本人だけが見る本人確認書類や請求書のように、厳しく制限すべきファイルもあります。公開範囲を考えずにストレージのURLをそのまま返すと、本来見えてはいけないファイルへアクセスできる可能性があります。ファイルアップロードでは、保存先だけでなくアクセス制御も設計対象です。

たとえば、公開画像はCDN経由のURLを返し、非公開ファイルは認証後に短時間だけ使える署名付きURLを返す、といった分け方があります。DBには公開可否や所有者IDを持たせ、API側でアクセス権限を確認する設計が安全です。

5. よくある落とし穴

5-1. サイズ制限を入れていない

ファイルアップロードで最も危険な落とし穴のひとつは、サイズ制限を入れていないことです。無制限に受け取る設計は、障害や悪用の原因になります。

大きなファイルを受け取ると、サーバーのメモリ、ディスク、ネットワーク帯域を大きく消費します。複数の大容量アップロードが重なると、通常のAPI処理にも影響が出ます。アップロード機能では、アプリの要件に合わせて上限サイズを決め、超えた場合は早めに拒否する必要があります。

たとえば、プロフィール画像なら2MB〜5MB程度で十分なことが多いです。一方、業務用PDFや動画では別の上限が必要になります。重要なのは「何でも受け取れるようにする」のではなく、用途に合った上限を明示することです。

5-2. 拡張子だけで安全判断してしまう

拡張子だけでファイルの安全性を判断するのは、不十分です。ファイル名はユーザーが自由に変えられるため、見た目だけでは中身を保証できません。

たとえば、.jpg という名前でも中身が本当に画像とは限りません。MIMEタイプも判断材料にはなりますが、クライアントから送られる情報だけを完全に信用するのは危険です。アップロード後に画像として読み込めるか確認する、許可形式を限定する、必要に応じて再エンコードするなどの対策が必要です。

初心者がやりがちなのは、「拡張子がpngならOK」とだけ実装することです。最低限、拡張子、MIMEタイプ、サイズを確認し、重要な機能ではファイル内容の検査も検討しましょう。

5-3. 大きなファイルでタイムアウトする

大きなファイルを扱う場合、アップロード時間やタイムアウトも設計に含める必要があります。小さな画像では動いても、大きなファイルで失敗することがあります。

ファイルサイズが大きいと、ブラウザからサーバーへ送る時間が長くなります。サーバーやプロキシ、ロードバランサ、クラウド環境にはリクエストサイズやタイムアウトの上限が設定されていることがあります。そのため、アプリ側だけでなく、インフラ側の制限にも注意が必要です。

たとえば、ローカルでは50MBのファイルを送れたのに、本番では413 Payload Too Largeやタイムアウトになることがあります。大きなファイルを扱う要件があるなら、上限サイズ、通信時間、再試行、直接ストレージアップロードなども検討しましょう。

6. まとめ

6-1. multipart/form-dataの基本整理

multipart/form-dataは、テキスト項目とファイルを複数のパートに分けて送るための形式です。ファイルアップロードAPIでは基本となる仕組みです。

ブラウザではFormDataを使うことで、ファイルと通常項目をまとめて送信できます。このとき、Content-Typeは手動で固定せず、boundaryを含めた設定をブラウザに任せるのが基本です。サーバー側では、ファイルと通常項目を分けて受け取り、サイズ、MIMEタイプ、保存先、公開範囲を確認します。

ファイルアップロードは、単に「ファイルを保存する機能」ではありません。外部入力を受け取る入口なので、バリデーション、エラー設計、保存名、アクセス制御まで含めて設計する必要があります。

6-2. ファイルアップロードAPIのチェックリスト

ファイルアップロードAPIを作るときは、送信、受信、検証、保存、公開範囲を分けて確認すると漏れを減らせます。

  • フロント側でFormDataを使っているか
  • FormData送信時にContent-Typeを手動固定していないか
  • フロントとAPIでファイル項目名が一致しているか
  • ファイルサイズの上限を設定しているか
  • MIMEタイプや拡張子を確認しているか
  • 必要に応じてファイル内容の検査をしているか
  • 元ファイル名をそのまま保存名にしていないか
  • 保存先と公開範囲を分けて設計しているか
  • エラー時にフロントが表示しやすいレスポンスを返しているか

このチェックリストを使うと、初心者が見落としやすいContent-Type、boundary、サイズ制限、保存名、公開範囲をまとめて確認できます。安全で扱いやすいアップロード機能にするには、動くことだけでなく、制限と運用まで含めて考えることが大切です。

7. 参考リンク