Nextjs server actions en profundidad

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:

  1. Build-time: Preparación y transformación
  2. Llamada desde el cliente
  3. Procesamiento y ejecucion en el servidor
  4. 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
  • 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. Este actionId 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 un moduleId (que apunta a un archivo JavaScript generado como "loader") y el nombre de exportación específico (que es el mismo actionId) 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
            }
          }
        }
      }
  • 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 donde tuAcción está definida y luego reexporta las funciones de servidor bajo los actionIds 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';

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 a serverActionReducer (found in packages/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 llamada POST 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 al actionId (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 es application/x-www-form-urlencoded para form submissions o text/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 dependencia react-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 el actionId.
    • se consulta la referencia en el server-reference-manifest.json (muchas veces se lo referencia como serverModuleMap en el codebase de Nextjs).
    • Con el actionId, se busca el moduleId correspondiente (que apunta hacia el archivo generado actions.js) y también el export name (el cual es el actionId mismo).
  • 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 (from react-server-dom-webpack/server.edge or react-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 header Host (o X-Forwarded-Host). Esto asegura que el request este viniendo de un trusted domain. Se puede configurar en next.config.js con la propiedad serverActions.allowedOrigins.
  • Cargar y ejecutar la acción:
    • El server de forma hace un require()s o import()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.
  • 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 un Location header.
    • Revalidación del Cache: Si llamas a revalidatePath('/my-path') o revalidateTag('my-tag'), el server va ejecutar una revalidación del cache. Ademas setea un x-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.
VER:  Context, Redux y Zustand en Next.js: ¿Cuál usar y cuándo?

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.

Loading

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *