React

Upload di file con un Route Handler nell’App Router

FormData in un Route Handler POST, limiti espliciti e storage esterno: come gestire l’upload di file nell’App Router senza sorprese.

In sintesi

  • Un upload va in un Route Handler `POST`, non in una Server Action con body grande.
  • Valida dimensione, tipo reale e nome prima di leggere i byte.
  • Il filesystem serverless è effimero: scrivi su object storage.

Un upload di file nell’App Router passa da un Route Handler POST che legge await request.formData(), controlla tipo e dimensione, e scrive sullo storage. Il tetto di 1 MB di default su una Server Action non è una preferenza: è un limite.

Perché non una Server Action

Una Server Action permette di alzare bodySizeLimit, ma il payload attraversa il layer di serializzazione di React. Per file grandi il Route Handler dà streaming, status HTTP espliciti e nessuna serializzazione in mezzo.

// app/api/upload/route.ts
import { NextResponse } from "next/server";
import { writeFile, mkdir } from "node:fs/promises";
import path from "node:path";

export const runtime = "nodejs";

export async function POST(request: Request) {
  const form = await request.formData();
  const file = form.get("file");
  if (!(file instanceof File)) {
    return NextResponse.json({ error: "missing file" }, { status: 400 });
  }
  if (file.size > 5 * 1024 * 1024) {
    return NextResponse.json({ error: "too large" }, { status: 413 });
  }
  const bytes = Buffer.from(await file.arrayBuffer());
  const name = crypto.randomUUID() + "-" + path.basename(file.name);
  await mkdir(path.join(process.cwd(), "uploads"), { recursive: true });
  await writeFile(path.join(process.cwd(), "uploads", name), bytes);
  return NextResponse.json({ name }, { status: 201 });
}

Cosa controllare davvero

ControlloPerchéCome
DimensioneEvita upload da gigabytefile.size prima di leggere i byte
Tipo realeL’header dichiarato menteMagic bytes o una libreria come file-type
Nome del filePath traversalpath.basename più un UUID
DestinazioneIl filesystem è effimeroBucket S3 o un volume montato

Il filesystem di una funzione serverless è di sola lettura tranne /tmp, e /tmp sparisce al riavvio dell’istanza. In produzione scrivi su object storage e restituisci una URL firmata, non un percorso locale.

Verifica con curl

curl -i -X POST http://localhost:3000/api/upload -F "file=@./prova.png"
# 201 con { "name": "..." }
# 400 senza campo file
# 413 oltre 5 MB

La risposta deve essere 201 con il nome, 400 senza file, 413 oltre il limite. Se vedi 500, il primo sospetto è il runtime: node:fs non gira su edge.

Il catalogo EneUI indicizza 111.985 componenti, 1.556 blocchi e 12.808 icone da 1.472 librerie; il dato è misurato il 1 novembre 2026 su ui.eneawork.it. Un form di upload è una primitiva, non un blocco: la parte visiva arriva dal catalogo, la parte di rete la scrivi.

Componenti per la parte visibile

Tre record: gli input per il campo file con stato di errore, i componenti form per il resto del modulo, e i blocchi form per una sezione completa con upload.

Se l’upload è l’effetto di un form, la scelta tra action e handler è spiegata in Route Handler o Server Action. L’errore «window is not defined» che salta fuori importando un modulo browser nel handler ha la sua causa in finestra non definita in Next.js. Per il caching attorno alla risposta, i quattro livelli di cache.

Il limite da mettere in conto

Un Route Handler che scrive su disco non scala oltre una istanza. Due richieste su due processi scrivono in due filesystem diversi e l’utente vede il file solo a intermittenza.

Domande frequenti

Posso fare upload con una Server Action?

Sì, alzando <code>bodySizeLimit</code> nella configurazione di Next. Resta la serializzazione in mezzo e nessuno streaming: per file sopra pochi MB il Route Handler è più adatto.

Come alzo il limite di dimensione?

Nel Route Handler il limite è quello che scrivi tu: controlla <code>file.size</code> e rispondi <code>413</code>. Il limite della piattaforma, per esempio quello del proxy, resta e va verificato a parte.

Perché il file non si trova dopo il deploy?

Perché il filesystem dell’istanza è effimero e cambia a ogni deploy o riavvio. Scrivi su object storage o su un volume persistente montato.

Come valido il tipo di file?

Leggi i primi byte e confrontali con la firma attesa, oppure usa <code>file-type</code>. Il <code>type</code> dichiarato dal browser è un’indicazione, non una prova.

Serve un upload multiplo?

Con più file in un solo <code>formData</code> leggi <code>form.getAll("file")</code> e valida ognuno. Oltre pochi file conviene una richiesta per file, così un errore non annulla tutto.