# 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.

> Pagina: https://ui.eneawork.it/blog/nextjs-upload-file-route-handler
> Markdown: https://ui.eneawork.it/blog/nextjs-upload-file-route-handler.md

*2026-11-01*

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

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.

Valida prima di leggere, leggi a blocchi, scrivi fuori dal processo. Se il file supera i 20 MB, l’upload diretto dal client allo storage con URL firmata è la strada.

## FAQ

### Posso fare upload con una Server Action?

Sì, alzando bodySizeLimit 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 file.size e rispondi 413. 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 file-type. Il type dichiarato dal browser è un’indicazione, non una prova.

### Serve un upload multiplo?

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