Las Server Actions de Next.js nos ofrecen una manera muy eficaz de manejar mutaciones y lógicas del servidor directamente desde nuestros componentes React. Esto hace que las líneas entre cliente/servidor estén un poco difusas, pero lo hace de una manera amigable para el dev. A priori parecen magia, pero debajo de cada llamada hay un sistema hiper sofisticado moviendo los engranajes para que esta magia funcione. En este posteo voy a intentar hacer un deep dive en cómo Next.js convierte estas llamadas directas de funciones en lo que es esencialmente interacciones API muy bien orquestadas.
Voy a asumir que tenés experiencia con React y Next, y que querés entender las mecánicas bajo el capot de las Server Actions.
La idea principal: Llamadas tipo RPC
En su núcleo, una accion servidor —una Server Action— te permite escribir una función asíncrona que se ejecuta en el servidor, pero la llamás desde un componente cliente, un componente con 'use client'
como si fuera una funcion local.
// app/my-page/page.tsx
export default function MyPage() {
async function myAction(formData: FormData) {
'use server'; // The magic directive!
const data = Object.fromEntries(formData);
// ... interact with database, perform server-side logic ...
console.log('Data received on server:', data);
// ... revalidate cache, redirect, or return data ...
}
return (
<form action={myAction}>
<input type="text" name="message" />
<button type="submit">Send to Server</button>
</form>
);
}
Lo lindo de todo esto es cómo se coloca la lógica de servidor junto con la interfaz gráfica que la llama. Pero ¿cómo es que myAction
realmente corre en el servidor cuando el cliente la invoca (en este caso, con el envío del formulario)?
La transformación de una abstracción amigable para el dev hacia una manera segura y eficiente de manejar la interacción entre cliente y servidor es una maravilla moderna de este hermoso framework llamado nextjs.
El camino de una Server Action
El proceso se puede desglosar en 4 etapas principales:
- Build-time: Preparación y transformación
- Llamada desde el cliente
- Procesamiento y ejecucion en el servidor
- Manejo del Response en el cliente
Vamos a ver cada etapa:
1. Build-time: Preparación y transformación (Turbopack & Rust)
Acá es donde Next.js (principalmente a través de Turbopack, su sistema de build hecho en Rust) arma la fundación:
- Descubrimiento: Este build process escanea todo tu código para encontrar las directivas
'use server'
. Esta directiva puede estar antes de una función async o al comienzo de un archivo, marcando todos los exports del mismo como Server Actions. - Transformación: Cuando se encuentra una Server Action, se lleva acabo una transformación.
- La parte crucial es que se inyecta metadata en el modulo. Generalmente se ve en la forma de un comentario (e.j.,
// __next_internal_action_entry_do_not_use__{"actionName": "...", ...}
) el cual ordena en una lista todas las funciones de ese modulo que se denominan como Server action
- La parte crucial es que se inyecta metadata en el modulo. Generalmente se ve en la forma de un comentario (e.j.,
- Generación de ID unico: Para cada Server Action, Next.js genera un ID con hash único (llamémoslo
actionId
). Este ID típicamente se deriva del path del archivo del módulo y del nombre de la función acción. EsteactionId
es crítico para el routing de la request en el servidor. - Generación del manifest (
server-reference-manifest.json
):- Se crea un JSON manifest file (e.g.,
dist/server/app/my-page/server-reference-manifest.json
). - Este manifiesto actúa como un mapa. Vincula cada
actionId
con información sobre cómo cargar y ejecutar la acción correspondiente. Esto incluye unmoduleId
(que apunta a un archivo JavaScript generado como "loader") y el nombre de exportación específico (que es el mismoactionId
) dentro de ese loader. - La estructura del manifest se veria algo asi (simplificado):
{ "node": { // Or "edge" for edge runtime "hashed_action_id_for_myAction": { "workers": { "app/my-page/page": { // Context/route key "moduleId": "./../../../../.next/server/app/my-page/actions.js", // Path to the loader "isAsync": true } }, "layer": { "app/my-page/page": "actionLayerName" // RSC layer information } } } }
- Se crea un JSON manifest file (e.g.,
- Generación del archivo Loader de acciones:
- Un archivo JavaScript que funciona como loader (e.g.,
.next/server/app/my-page/actions.js
) se genera para la ruta. - Este loader no tiene la lógica de las acciones directamente. Lo que hace es que, dinámicamente,
import()
a el módulo original dondetuAcción
está definida y luego reexporta las funciones de servidor bajo losactionId
s generados previamente.// Example: .next/server/app/my-page/actions.js (conceptual) // (Dynamically imports your actual page.tsx or actions.ts) // and re-exports actions using their hashed IDs export { myActionFromOriginalModule as hashed_action_id_for_myAction } from './../../../../app/my-page/page';
- Un archivo JavaScript que funciona como loader (e.g.,
2. Llamada del lado del cliente
Cuando tu código del lado del cliente (por ejemplo, el envío de un formulario o una llamada directa desde un componente cliente) invoca una Server Action:
- Función proxy: No estas llamando a tu código del server directamente. React y Nextjs crean una funcion proxy en el cliente.
serverActionReducer
&fetchServerAction
: Este proxy cuando se llama, típicamente involucra aserverActionReducer
(found inpackages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts
).fetchServerAction
es la función dentro de este reducer que se ocupa de construir y enviar el request.- HTTP
POST
Request: Una llamadaPOST
se hace al URL de la pagina actual (o del URL asociado con el Server Action si este esta definido en un archivo separado) - Headers HTTP críticos:
Next-Action
: Este header esta seteado alactionId
(el ID único hasheado que hablamos arriba) del Server Action que estamos invocando. Este es el principal mecanismo el cual el server usa para identificar que acción ejecutar.Content-Type
: Generalmente esapplication/x-www-form-urlencoded
para form submissions otext/x-component
para acciones RSC-related. El server tiene que saber como 'parsear' el body.Next-Router-State-Tree
: Acá se envía el estado actual del router del cliente (una representación serializada del component tree). Esto le permite al server entender el contexto del cliente el cual es importante para actualizar y hacer diffs de los RSC.- Se envian otros headers para RSC como
RSC_HEADER
- Request Body:
- Si la llamada se ejecuta por un form action, automáticamente se serializa y se manda como el body.
- Si se llama de manera directa (ej.,
myAction(args)
), los argumentos se serializan usando un mecanismo comoencodeReply
que sale de la dependenciareact-server-dom-webpack
.
3. Manejo en el Server-Side y ejecución
El server de Next.js (tanto el runtime Node.js o Edge) recibe el POST
request:
- Request Reception & Parsing: El server parsea el request entrante.
- Se identifica la acción (
packages/next/src/server/app-render/action-handler.ts
):- El server lee el header
Next-Action
y obtiene elactionId
. - se consulta la referencia en el
server-reference-manifest.json
(muchas veces se lo referencia comoserverModuleMap
en el codebase de Nextjs). - Con el
actionId
, se busca elmoduleId
correspondiente (que apunta hacia el archivo generadoactions.js
) y también el export name (el cual es elactionId
mismo).
- El server lee el header
- Deserialización de los argumentos: El request body (que tiene el formdata o los argumentos serializados) se deserializa. For direct invocations, a server-side counterpart to
encodeReply
(fromreact-server-dom-webpack/server.edge
orreact-server-dom-webpack/server.node
) is used. - Security Checks (CSRF Protection):
- Next.js chequea y tiene protección CSRF (Cross-Site Request Forgery). Típicamente chequea que el header
Origin
del request matchee el headerHost
(oX-Forwarded-Host
). Esto asegura que el request este viniendo de un trusted domain. Se puede configurar ennext.config.js
con la propiedadserverActions.allowedOrigins
.
- Next.js chequea y tiene protección CSRF (Cross-Site Request Forgery). Típicamente chequea que el header
- Cargar y ejecutar la acción:
- El server de forma hace un
require()
s oimport()
s del archivo loader identificado (actions.js
). - Luego se accede al export específico del loader usando el
actionId
. - Este export es la referencia a la funcion original del Server Action: (ej.,
myAction
). - Finalmente se ejecuta la accion con los argumentos de-serializados.
- El server de forma hace un
- Worker Forwarding (Casos especiales): En ambientes distribuidos (ej: edge deployments con varios workers), si el worker que recibe el request inicialmente no tiene el codigo de la accion especifica localmente, nextjs puede hacer un forward del request incluyendo headers y body al worker que si lo tiene. Se le agrega un header especial para manejar de forma correcta este routing interno,
x-action-forwarded: 1
. - Manejo del resultado:
- Devolver la data y actualizar el UI (RSC): Si la acción devuelve data que debería actualizar el UI, esta data generalmente se empaqueta como un RSC "flight" payload. La utlididad
generateFlight
de Nextjs se usa para esto, el payload representa el diff de los cambios para el UI. - Redireccionamientos: Si la accion llama
redirect('/new-path')
, Nextjs interpreta esta redireccion especial y prepara una respuesta HTTP apropiada. Le agrega el codigo correcto 307-308 y le agrega unLocation
header. - Revalidación del Cache: Si llamas a
revalidatePath('/my-path')
orevalidateTag('my-tag')
, el server va ejecutar una revalidación del cache. Ademas setea unx-action-revalidated
header en el response para avisarle al cliente. - Cookies: Cualquier modificación de las cookies, sea set o delete, durante la acción se mandan al cliente via un
Set-Cookie
headers en el response.
- Devolver la data y actualizar el UI (RSC): Si la acción devuelve data que debería actualizar el UI, esta data generalmente se empaqueta como un RSC "flight" payload. La utlididad
4. Procesamiento del Response en el Client-Side
El cliente recibe el response del server:
- RSC Payload: Si el server manda un RSC flight payload (Content-Type
text/x-component
), React de manera inteligente va mergear estos updates dentro del client-side component tree. Esto sucede sin hacer un full page reload. Muy suave, fluido para el usuario. - Redireccionamiento: Si el server tiene un redirect status y el
Location
header, el router del cliente de nextjs maneja la navegación hacia el url apropiado. - Señales de revalidación: El cliente inspecciona el el header
x-action-revalidated
header. Esto le indica al cliente que invalide cualquier data relacionada a tags o paths que pueda estar estancada en el cache del router. - Cookies: De manera automatica el navegador va procesar cualquier
Set-Cookie
headers. - Manejo de errores: Si la acción resulta en un error no manejado, unhandled error, el archivo
error.js
más cercano lo va atrapar o si hay un<Suspense>
boundary este lo atrapa dependiendo del setup.
El beneficio de esta complejidad
Estos son los beneficios de este sistema complejo:
- Mejor experiencia para el dev: Al poder tener la logica del server al lado del cliente, te ahorras el paso de la api, el fetch y demas, el modelo mental es mas simple, y reducis el cambio de contexto (rebotar entre cliente servidor).
- Mejoras progresivas: Cuando las server actions se usan con HTML
<form>
, funcionan incluso si JS esta desactivado o no se cargo todavía. - Menos JavaScript en el cliente: Hay menos
fetch
en el codigo, menos manejo de estado para loading o errores, y se necesita menos logica para sincronizar la data en el cliente. - Seguridad desde el diseño: Protección CSRF incluida por defecto. El uso de IDs hasheados y routing basada en un manifest agrega una capa de obfuscación para tus rutas.
- Integración con los features de Next: Funciona muy facilmente con el cache, revalidación, redireccionamiento del app router de Nextjs.
Conclusión
Las Server Actions de Nextjs no son syntactic sugar, son mucho más. Representan un mecanismo sofisticado tipo RPC, el cual esta integrado en un nivel muy profundo al runtime y build de Nextjs. Al entender el trayecto completo de una simple función en un componente a como se transforma en un POST
request manejado por un dispatcher del serverside que se basa en un manifest, y finalmente como el cliente actualiza la UI o la navegación, vas a poder usar mejor aún esta herramienta y la vas a poder debugear con mayor criterio. La transformación de una abstracción amigable para el dev hacia una manera segura y eficiente de manejar la interacción entre cliente y servidor es una maravilla moderna de este hermoso framework llamado nextjs.