Guia de Implementação de Upload de Arquivos em Next.js | App Router, API Route e Suporte a S3
Com o surgimento do Next.js App Router, os padrões de implementação de upload de arquivos mudaram significativamente. O <code>multer</code>, usado na era do Pages Router, não pode ser usado diretamente nos Route Handlers do App Router, e a implementação com a API <code>FormData</code> padrão da Web se tornou predominante. Este artigo explica sistematicamente desde Server Actions do App Router, Route Handlers (route.ts) e barras de progresso do lado do cliente até uploads para Vercel Blob e AWS S3.
Upload de arquivo usando Server Actions no App Router
Server Actions são funções de servidor com a diretiva <code>"use server"</code> que podem ser passadas diretamente ao <code>action</code> do formulário. Funcionam sem JavaScript e são compatíveis com melhoramento progressivo.
// 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>
);
}
Processamento de FormData em Route Handlers (route.ts)
Ao usar como API, crie um Route Handler em <code>app/api/upload/route.ts</code>. O <code>multer</code> não é necessário e você pode obter o <code>FormData</code> padrão da Web usando <code>request.formData()</code>.
// 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,
},
};
Implementação de barra de progresso no lado do cliente
A Fetch API não pode obter o progresso do upload por padrão. Existem métodos usando o evento <code>upload.onprogress</code> do <code>XMLHttpRequest</code> ou <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 para Vercel Blob
No ambiente Vercel, usando o pacote <code>@vercel/blob</code>, você pode fazer upload direto para o object storage gerenciado pelo Vercel. Uma funcionalidade de 「upload do cliente」 também é fornecida para contornar o limite de 4.5MB da Vercel Function.
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 para AWS S3 (método de URL pré-assinada)
Para arquivos grandes ou infraestrutura fora do Vercel, usar URL Presigned do S3 para fazer upload direto do cliente para o S3 é o método mais eficiente.
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;
}
Configuração em next.config.js
No Next.js, é possível configurar definições relacionadas a upload de arquivos em <code>next.config.js</code>. Porém, ao fazer deploy no Vercel, as configurações em <code>vercel.json</code> têm prioridade.
// 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;
Arquivo de teste disponível para usar neste artigo (gratuito)
- → <a href="/ja/files/images/png/1mb/" class="text-primary-600 dark:text-primary-400 hover:underline">Imagem PNG de teste (1MB)</a> — Para teste de verificação de tipo MIME e tamanho
- → <a href="/ja/files/pdf/1mb/" class="text-primary-600 dark:text-primary-400 hover:underline">Arquivo de teste PDF (1MB)</a> — Para confirmação de validação de upload de PDF
- → <a href="/ja/files/zip/1mb/" class="text-primary-600 dark:text-primary-400 hover:underline">Arquivo de teste ZIP (1MB)</a> — Para validação de formato de arquivo de upload S3
Artigos relacionados
- → <a href="/ja/blog/vercel-upload-config/" class="text-primary-600 dark:text-primary-400 hover:underline">Configuração e limites de upload de arquivos do Vercel | Vercel Blob, limitações de API e soluções alternativas</a>
- → <a href="/ja/blog/s3-upload-limit/" class="text-primary-600 dark:text-primary-400 hover:underline">Resumo dos limites de upload de arquivos do AWS S3 e CloudFront</a>
- → <a href="/ja/blog/file-validation-checklist/" class="text-primary-600 dark:text-primary-400 hover:underline">Lista de verificação de implementação de validação de arquivo para formulários web</a>