RPC type-safe con i Server Actions di Next.js: via il layer API
Un Server Action è già una RPC. Ecco come renderlo type-safe da capo a fondo e validato a runtime, senza route API né tipi di risposta da mantenere.
Da un componente React puoi chiamare una funzione del server come ne chiami una locale: passi gli argomenti, leggi il valore di ritorno e lasci che il compilatore controlli i due lati. È ciò che offre un Server Action in Next.js. Una RPC type-safe con i Server Actions di Next.js significa che la funzione sul server e la chiamata sul client condividono un unico tipo: niente route API scritta a mano, niente wrapper di fetch, nessuna forma di risposta da tenere allineata. La richiesta di rete c'è ancora. Smetti solo di scriverla a mano.
Alla fine di questa guida avrai una funzione del server tipizzata da capo a fondo nell'editor, che rifiuta input non validi a runtime, che controlla chi la chiama prima di eseguire e che restituisce errori tipizzati invece di lanciare eccezioni. È sull'ultimo passo che molti team si fermano troppo presto, ed è lì che si annidano i buchi di sicurezza.
Cosa significa type-safe qui, e cosa no
Un Server Action è una RPC. Il team di tRPC lo descrive allo stesso modo: scrivi una funzione sul backend e la chiami dal frontend con il livello di rete reso invisibile. Il punto è che un tipo TypeScript è solo un contratto in fase di compilazione. I tipi spariscono prima che il codice giri, quindi vincolano il tuo codice, mai chi invia la richiesta.
Ogni funzione marcata 'use server' diventa un endpoint HTTP pubblico. Chiunque può fargli una POST con qualunque payload: null, un oggetto al posto di una stringa, un blob da 10 MB. L'action è un endpoint pubblico, e i confini dei componenti, i controlli lato client e le firme delle funzioni non contano nulla per una richiesta inviata con curl. Quindi type-safe nell'editor e validato sul filo sono due proprietà diverse. Il pattern qui sotto le ottiene entrambe da un'unica fonte di verità.
Costruirlo in cinque passi
Passo 1: tratta l'action come un endpoint pubblico
Prima di scrivere il corpo della funzione, accetta cos'è: una route POST senza autenticazione. Dentro ogni action che tocca dati servono tre controlli: validazione dell'input, verifica dell'autenticazione (a meno che l'action sia davvero pubblica) e verifica dell'autorizzazione su proprietà o permessi. Mettili nell'action, non nel componente che la chiama. Il componente gira su un dispositivo che non controlli.
Passo 2: scrivi il contratto come schema, non come tipo
Definisci l'input con uno schema di validazione. Zod è la scelta più diffusa; Valibot e ArkType si comportano allo stesso modo tramite Standard Schema. Un solo schema dà due cose insieme: un tipo statico che il compilatore deduce e un controllo a runtime che il server esegue a ogni chiamata. Il tipo si deduce dallo schema mentre l'input in arrivo viene validato contro lo stesso schema, così i due non possono divergere.
const profileSchema = z.object({ name: z.string().min(1).max(80), bio: z.string().max(280) })Lo schema si scrive una volta. Il componente, l'action e la chiamata al database leggono tutti la stessa forma.
Passo 3: avvolgi ogni action in un unico client
Cablare validazione e autenticazione a mano in ogni action si sfalda nel giro di un mese. Una libreria come next-safe-action dà un solo action client con middleware .use() componibili e un contesto tipizzato che arriva fino all'handler. Colleghi autenticazione e rate limiting una volta, e ogni action costruita sul client li eredita. Lo schema garantisce che parsedInput sia tipizzato e validato prima che il tuo codice giri.
export const updateProfile = actionClient.schema(profileSchema).action(async ({ parsedInput, ctx }) => { return db.profile.update(ctx.user.id, parsedInput) })Passo 4: chiamala dal componente e gestisci gli errori di validazione
Sul client chiami l'action tramite un hook. Anche useActionState di React 19 va bene. Quando l'input non passa lo schema, torna al client un oggetto validationErrors con ogni campo fallito e il relativo messaggio, così il form mostra gli errori inline senza un secondo viaggio sulla rete. L'utilità flattenValidationErrors() rimodella quell'oggetto sulla forma della tua libreria di form.
const { execute, result } = useAction(updateProfile)Passo 5: restituisci errori tipizzati, non lanciarli
Un'eccezione lanciata attraversa il confine di rete come errore generico del server e perde il suo tipo sul client. Restituisci invece un oggetto risultato tipizzato, con un ramo di successo e uno di fallimento. Così il componente si dirama sull'esito con il compilatore che controlla entrambe le strade, e non finisce mai un messaggio di eccezione grezzo nel browser.
Come verificare che funzioni davvero
Tre prove confermano che il contratto tiene:
- Prova nell'editor. Cambia lo schema o la forma del valore di ritorno. Il punto di chiamata deve diventare rosso prima di salvare. Se non succede, il tipo non scorre da capo a fondo.
- Prova con curl. Manda una POST con un payload malformato direttamente all'endpoint dell'action, saltando il form. Un'action corretta la rifiuta con un errore di validazione. Una rotta va in crash o scrive spazzatura nel database.
- Prova di autenticazione. Chiama l'action senza sessione. Deve rifiutare prima di eseguire qualsiasi logica, perché il controllo vive dentro l'action.
Errori frequenti e come correggerli
Fidarsi del tipo come guardia. L'errore più comune in assoluto. Un tipo TypeScript è documentazione che il compilatore impone al tuo codice, niente di più. Aggiungi lo schema a runtime anche quando il tipo sembra a prova di bomba.
Nessuna autenticazione dentro l'action. L'autenticazione nella pagina non protegge l'endpoint. Sposta il controllo di sessione nell'action o nel suo middleware.
Lasciar uscire campi di troppo. Restituire un'intera riga del database manda colonne che il client non dovrebbe mai vedere. Restituisci una forma esplicita, oppure valida anche l'output.
Lanciare eccezioni per il flusso normale. Tieni le eccezioni per i guasti veri. Usa un valore di ritorno tipizzato per gli esiti previsti, come una validazione fallita o un permesso negato.
Usare le action per letture pesanti. I Server Actions girano in sequenza e non vengono raggruppati. Una dashboard che spara dieci letture in parallelo tramite action risulterà lenta.
Quando un Server Action è lo strumento sbagliato
I Server Actions stanno bene per le mutation e per le pagine che restano quasi statiche dopo il primo caricamento. Per un'interfaccia ricca di dati con paginazione, filtri e ordinamento che rifà le query dopo il caricamento, o per un backend che serve anche un'app mobile, tRPC con TanStack Query è la scelta migliore: raggruppa le chiamate concorrenti in una sola richiesta e serve più di un client con piena type safety. La regola onesta è leggere gratis dai server component, modificare con action type-safe e passare a tRPC quando il lato lettura sviluppa esigenze proprie.
Per approfondire
Il passo di validazione qui è la stessa guardia che chiude la maggior parte delle vulnerabilità dei Server Action. Abbiamo percorso l'intera superficie d'attacco in sicurezza dei Server Actions di Next.js. Se stai ancora decidendo quale logica debba stare sul server, l'albero decisionale tra Server e Client Components è il pezzo che fa coppia con questo.
Domande frequenti
- Serve ancora Zod se il mio Server Action è già tipizzato in TypeScript?
- Sì. Un tipo TypeScript esiste solo mentre il codice compila. Sparisce prima che la funzione giri, quindi vincola il tuo codice e nulla di ciò che arriva all'endpoint dall'esterno. Un Server Action è una route HTTP pubblica, e un attaccante può fare una POST con qualunque payload direttamente da curl. Lo schema Zod è il controllo a runtime che il tipo non può fare. Tieni entrambi: lo schema valida a runtime e il compilatore deduce il tipo dallo stesso schema.
- Un Server Action è più lento di una chiamata tRPC?
- Per una singola mutation la differenza è trascurabile. Il divario emerge sotto carico: i Server Actions girano in sequenza e non vengono raggruppati, quindi una schermata che lancia più action insieme paga un viaggio per ciascuna. tRPC raggruppa le chiamate concorrenti in un'unica richiesta HTTP. Se l'interfaccia fa molte letture in parallelo dopo il primo caricamento, quel raggruppamento conta. Per un salvataggio o un aggiornamento su un form, il Server Action è lo strumento più leggero.
- Posso riusare un Server Action sia per un'app web sia per un'app mobile?
- Non in modo pulito. Un Server Action è legato alla route Next.js che lo definisce e viene invocato attraverso il protocollo del framework, non tramite un'API pubblica stabile chiamabile da un client nativo. Se un'app mobile serve la stessa logica, esponila con un route handler o una procedura tRPC che entrambi i client possono chiamare, e tieni il Server Action come sottile involucro sulla funzione condivisa. La logica di business sta in una funzione semplice; l'action e l'API sono due porte verso di essa.
- Come mostro gli errori di validazione del form senza lanciare eccezioni dall'action?
- Restituiscili invece di lanciarli. Con next-safe-action, uno schema fallito restituisce un oggetto validationErrors che elenca ogni campo e il suo messaggio, e flattenValidationErrors() lo rimodella per la tua libreria di form. Sul client, useActionState di React 19 o l'hook della libreria espone quel risultato, così il form mostra gli errori inline. Un'eccezione lanciata attraverserebbe il confine come errore generico del server e perderebbe il dettaglio per campo, proprio ciò che a un form serve mostrare.
Studio
Inizia un progetto.
Un partner unico per il prodotto digitale che devi costruire. Produzione più veloce, tecnologie moderne, costi ridotti. Un team, una fattura.