Skip to content

Implementierungsleitfaden für Datei-Upload in Next.js | App Router, API Route und S3-Unterstützung

Kategorie:Next.js · React
Dieser Artikel ist derzeit nur auf Japanisch verfügbar. Übersetzte Versionen werden schrittweise veröffentlicht.

Mit der Einführung des Next.js App Router haben sich die Implementierungsmuster für Datei-Uploads erheblich verändert. Das im Pages-Router-Zeitalter verwendete <code>multer</code> kann nicht direkt in App Router Route Handlers verwendet werden, und die Implementierung mit der Web-Standard-API <code>FormData</code> ist zum Standard geworden. Dieser Artikel erläutert systematisch von Server Actions des App Router, Route Handlers (route.ts) und Client-seitigem Fortschrittsbalken bis zum Upload zu Vercel Blob und AWS S3.

Browser FormData Server Action "use server" Route Handler app/api/.../route.ts Vercel Blob S3 R2 / GCS
Abbildung: Upload-Fluss von Next.js App Router (Server Actions / Route Handlers)

Datei-Upload mit Server Actions im App Router

Server Actions sind Serverfunktionen mit der Direktive <code>"use server"</code>, die direkt an die <code>action</code> des Formulars übergeben werden können. Sie funktionieren ohne JavaScript und sind kompatibel mit progressiver Verbesserung.

// app/upload/actions.ts
'use server';

import { writeFile } from 'fs/promises';
import { join } from 'path';

export async function uploadFile(formData: FormData) {
  const file = formData.get('file') as File;

  if (!file || file.size === 0) {
    return { error: 'ファイルが選択されていません。' };
  }

  // ファイルサイズの検証(10MB 上限)
  const MAX_SIZE = 10 * 1024 * 1024;
  if (file.size > MAX_SIZE) {
    return { error: 'ファイルサイズは10MB以下にしてください。' };
  }

  // MIMEタイプの検証
  const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'application/pdf'];
  if (!allowedTypes.includes(file.type)) {
    return { error: '許可されていないファイル形式です。' };
  }

  // Buffer に変換してローカルに保存
  const bytes = await file.arrayBuffer();
  const buffer = Buffer.from(bytes);

  // ファイル名をサニタイズしてユニークな名前を生成
  const timestamp = Date.now();
  const safeName = file.name.replace(/[^a-zA-Z0-9._-]/g, '_');
  const filename = `${timestamp}_${safeName}`;
  const uploadDir = join(process.cwd(), 'public', 'uploads');
  await writeFile(join(uploadDir, filename), buffer);

  return { success: true, filename, url: `/uploads/${filename}` };
}
// app/upload/page.tsx
import { uploadFile } from './actions';

export default function UploadPage() {
  return (
    <form action={uploadFile}>
      <input type="file" name="file" accept="image/*,application/pdf" />
      <button type="submit">アップロード</button>
    </form>
  );
}

FormData-Verarbeitung in Route Handlers (route.ts)

Bei Verwendung als API erstellen Sie einen Route Handler in <code>app/api/upload/route.ts</code>. <code>multer</code> ist nicht erforderlich und Sie können das Web-Standard-<code>FormData</code> mit <code>request.formData()</code> abrufen.

// app/api/upload/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { writeFile, mkdir } from 'fs/promises';
import { join } from 'path';
import { existsSync } from 'fs';

export async function POST(request: NextRequest) {
  try {
    const formData = await request.formData();
    const file = formData.get('file') as File | null;

    if (!file) {
      return NextResponse.json({ error: 'ファイルが見つかりません。' }, { status: 400 });
    }

    // ファイルサイズ検証
    const MAX_SIZE = 5 * 1024 * 1024; // 5MB
    if (file.size > MAX_SIZE) {
      return NextResponse.json(
        { error: `ファイルサイズは ${MAX_SIZE / 1024 / 1024}MB 以下にしてください。` },
        { status: 413 }
      );
    }

    // MIMEタイプ検証(クライアントの申告値は信頼せず、実際の内容で判定するのが理想)
    const allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'application/pdf'];
    if (!allowedMimes.includes(file.type)) {
      return NextResponse.json({ error: '許可されていないファイル形式です。' }, { status: 415 });
    }

    const bytes = await file.arrayBuffer();
    const buffer = Buffer.from(bytes);

    // アップロードディレクトリの作成(存在しない場合)
    const uploadDir = join(process.cwd(), 'public', 'uploads');
    if (!existsSync(uploadDir)) {
      await mkdir(uploadDir, { recursive: true });
    }

    // ユニークなファイル名を生成
    const uniqueSuffix = `${Date.now()}-${Math.round(Math.random() * 1e9)}`;
    const ext = file.name.split('.').pop();
    const filename = `upload-${uniqueSuffix}.${ext}`;
    await writeFile(join(uploadDir, filename), buffer);

    return NextResponse.json({
      success: true,
      filename,
      url: `/uploads/${filename}`,
      size: file.size,
      type: file.type,
    });
  } catch (error) {
    console.error('Upload error:', error);
    return NextResponse.json({ error: 'アップロード中にエラーが発生しました。' }, { status: 500 });
  }
}

// Vercel のデフォルトは 4.5MB のため、必要に応じて設定を変更
export const config = {
  api: {
    bodyParser: false,
  },
};

Implementierung der Fortschrittsanzeige auf der Clientseite

Die Fetch API kann standardmäßig den Upload-Fortschritt nicht abrufen. Es gibt Methoden mit dem <code>upload.onprogress</code>-Event von <code>XMLHttpRequest</code> oder <code>ReadableStream</code>.

// components/UploadWithProgress.tsx
'use client';

import { useState } from 'react';

export default function UploadWithProgress() {
  const [progress, setProgress] = useState(0);
  const [uploading, setUploading] = useState(false);
  const [done, setDone] = useState(false);

  const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file) return;

    const formData = new FormData();
    formData.append('file', file);
    setUploading(true);
    setProgress(0);

    const xhr = new XMLHttpRequest();
    xhr.upload.onprogress = (event) => {
      if (event.lengthComputable) {
        setProgress(Math.round((event.loaded / event.total) * 100));
      }
    };
    xhr.onload = () => { setUploading(false); setDone(true); };
    xhr.open('POST', '/api/upload');
    xhr.send(formData);
  };

  return (
    <div>
      <input type="file" onChange={handleUpload} />
      {uploading && (
        <div>
          <div className="progress-bar" style={{ width: progress + '%' }} />
          <span>{progress}%</span>
        </div>
      )}
      {done && <p>アップロード完了</p>}
    </div>
  );
}

Upload zu Vercel Blob

In der Vercel-Umgebung können Sie mit dem Paket <code>@vercel/blob</code> direkt zu Vercel verwalteten Object Storage hochladen. Eine 「Client-Upload」-Funktionalität wird ebenfalls bereitgestellt, um das 4,5-MB-Limit von Vercel Function zu umgehen.

npm install @vercel/blob
// app/api/upload/route.ts(Vercel Blob 使用)
import { put } from '@vercel/blob';
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
  const formData = await request.formData();
  const file = formData.get('file') as File;

  if (!file) {
    return NextResponse.json({ error: 'ファイルが見つかりません。' }, { status: 400 });
  }

  // Vercel Blob にアップロード(BLOB_READ_WRITE_TOKEN が必要)
  const blob = await put(file.name, file, {
    access: 'public',       // 'public' or 'private'
    addRandomSuffix: true,  // ファイル名の衝突を防ぐ
  });

  return NextResponse.json({ url: blob.url, size: blob.size });
}

// 大容量ファイルのためにボディサイズ上限を引き上げ(Vercel では効果なし)
export const maxDuration = 60; // 秒
// クライアントアップロード(4.5MB超のファイルを Vercel Blob に直接送信)
// app/api/upload/route.ts
import { handleUpload, type HandleUploadBody } from '@vercel/blob/client';
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest): Promise<NextResponse> {
  const body = (await request.json()) as HandleUploadBody;

  try {
    const jsonResponse = await handleUpload({
      body,
      request,
      onBeforeGenerateToken: async (pathname) => ({
        allowedContentTypes: ['image/jpeg', 'image/png', 'application/pdf'],
        maximumSizeInBytes: 50 * 1024 * 1024, // 50MB
      }),
      onUploadCompleted: async ({ blob, tokenPayload }) => {
        console.log('アップロード完了:', blob.url);
      },
    });

    return NextResponse.json(jsonResponse);
  } catch (error) {
    return NextResponse.json({ error: String(error) }, { status: 400 });
  }
}

Upload zu AWS S3 (Methode mit vorsignierter URL)

Für große Dateien oder Infrastruktur außerhalb von Vercel ist die Verwendung einer Presigned URL von S3 zum direkten Upload vom Client zu S3 die effizienteste Methode.

npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
// app/api/presigned-url/route.ts
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { NextRequest, NextResponse } from 'next/server';

const s3 = new S3Client({
  region: process.env.AWS_REGION!,
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
  },
});

export async function POST(request: NextRequest) {
  const { filename, contentType, size } = await request.json();

  // サーバー側でのバリデーション
  const MAX_SIZE = 100 * 1024 * 1024; // 100MB
  if (size > MAX_SIZE) {
    return NextResponse.json({ error: 'ファイルが大きすぎます。' }, { status: 413 });
  }

  const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'application/pdf', 'application/zip'];
  if (!allowedTypes.includes(contentType)) {
    return NextResponse.json({ error: '許可されていない形式です。' }, { status: 415 });
  }

  const key = `uploads/${Date.now()}-${filename}`;

  const command = new PutObjectCommand({
    Bucket: process.env.AWS_BUCKET_NAME!,
    Key: key,
    ContentType: contentType,
    ContentLength: size,
  });

  // 15分間有効な署名付きURLを生成
  const presignedUrl = await getSignedUrl(s3, command, { expiresIn: 900 });

  return NextResponse.json({ presignedUrl, key });
}
// クライアントから S3 に直接アップロード
async function uploadToS3(file: File) {
  // 1. サーバーから Presigned URL を取得
  const res = await fetch('/api/presigned-url', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      filename: file.name,
      contentType: file.type,
      size: file.size,
    }),
  });
  const { presignedUrl, key } = await res.json();

  // 2. S3 に直接 PUT(Vercel Function を経由しないため上限なし)
  const uploadRes = await fetch(presignedUrl, {
    method: 'PUT',
    headers: { 'Content-Type': file.type },
    body: file,
  });

  if (!uploadRes.ok) throw new Error('S3 アップロード失敗');
  return key;
}

Konfiguration in next.config.js

In Next.js können dateiupload-bezogene Einstellungen in <code>next.config.js</code> konfiguriert werden. Beim Deployment auf Vercel haben Einstellungen in <code>vercel.json</code> jedoch Vorrang.

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  // 外部画像の許可(S3 のドメインなど)
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: '*.s3.amazonaws.com',
      },
      {
        protocol: 'https',
        hostname: '*.public.blob.vercel-storage.com',
      },
    ],
  },
  // Pages Router の API Route のボディサイズ上限(App Router には無効)
  // App Router では Route Handler に対して個別に設定する
  experimental: {
    serverActions: {
      bodySizeLimit: '10mb', // Server Actions のボディサイズ上限
    },
  },
};

module.exports = nextConfig;

Testdatei zur Verwendung in diesem Artikel (kostenlos)

  • → <a href="/ja/files/images/png/1mb/" class="text-primary-600 dark:text-primary-400 hover:underline">PNG-Testbild (1MB)</a> — Zum Test der MIME-Typ- und Größenvalidierung
  • → <a href="/ja/files/pdf/1mb/" class="text-primary-600 dark:text-primary-400 hover:underline">PDF-Testdatei (1MB)</a> — Zur Überprüfung der PDF-Upload-Validierung
  • → <a href="/ja/files/zip/1mb/" class="text-primary-600 dark:text-primary-400 hover:underline">ZIP-Testdatei (1MB)</a> — Für S3-Upload- und Dateiformat-Validierung

Verwandte Artikel

  • → <a href="/ja/blog/vercel-upload-config/" class="text-primary-600 dark:text-primary-400 hover:underline">Vercel-Datei-Upload-Konfiguration und Limits | Vercel Blob, API-Limits und Lösungsansätze</a>
  • → <a href="/ja/blog/s3-upload-limit/" class="text-primary-600 dark:text-primary-400 hover:underline">Zusammenfassung der Datei-Upload-Limits von AWS S3 und CloudFront</a>
  • → <a href="/ja/blog/file-validation-checklist/" class="text-primary-600 dark:text-primary-400 hover:underline">Checkliste zur Implementierung der Dateiverifizierung für Web-Formulare</a>