Web Design and Engineering12 de agosto de 20267 min de lectura

Server Actions de Next.js en producción: qué usamos y qué evitamos

Cada Server Action que exportas es un endpoint POST público sin autenticación. Estos son los patrones de Next.js 16 que llevamos a producción y los que rechazamos.

Yellow and green cables are neatly connected.

Una Server Action de Next.js es una función de servidor que llamas como si fuera local, y eso esconde el detalle que decide si tu app es segura: cada llamada es una petición POST pública por la red.

Se eligen las Server Actions porque quitan de en medio la capa de API. Un formulario llama a una función, la función escribe en la base de datos, la página se actualiza. Sin route handler, sin fetch en el cliente, sin contrato de petición que mantener sincronizado. La ventaja es real, y usamos Server Actions en la mayoría de los proyectos SaaS. También es donde empiezan los errores. La función parece código privado, así que se trata como código privado. No lo es. Estos son los patrones que corremos en producción con Next.js 16 y los que rechazamos, después de que el framework se asentara con React 19 y el modelo de caché de Next.js 16.

Por qué una Server Action es más arriesgada de lo que parece

Cuando marcas una función con la directiva 'use server' y la exportas, Next.js la compila en un endpoint. Cada action se vuelve un endpoint POST público sin autenticación, alcanzable con una petición directa y no solo a través de tu formulario. Next.js añade una defensa a nivel de framework: compara el Origin de la petición con el Host (o X-Forwarded-Host) y rechaza si no coinciden, lo que bloquea los POST cross-site más básicos. Si trabajas detrás de un proxy o una CDN en otro dominio, los indicas en serverActions.allowedOrigins. La protección del framework termina ahí. No sabe quién es tu usuario, qué posee, ni si esa fila es suya para tocarla. Como dice la guía de seguridad de Next.js, las protecciones del framework no sustituyen los controles a nivel de aplicación.

Por qué "es solo una función" es el modelo mental equivocado

El arreglo más común es proteger el límite una sola vez, en el middleware o en un layout, dando por hecho que todo lo que está debajo queda protegido. No es así. El middleware corre en la navegación, no en cada invocación de la action, y un control en el layout protege lo que se renderiza, no lo que un atacante manda por POST directamente. Trata cada action como su propia puerta de entrada. El principio es uno: el perímetro es por action, no por ruta.

Los patrones que usamos

Valida la entrada con Zod y luego comprueba la autorización aparte

Validación y autorización son dos trabajos distintos, y confundirlos es el fallo más frecuente que vemos en las Server Actions. Un objeto bien formado todavía puede apuntar a una fila que quien llama no posee. Así que hacemos las dos cosas, en orden: analizamos el payload con un esquema Zod y luego verificamos que quien llama pueda actuar sobre ese recurso concreto.

'use server'
export async function deleteProject(input) {
  const { id } = DeleteSchema.parse(input)   // validacion
  const user = await requireUser()           // autenticacion
  const project = await projects.findById(id)
  if (project.ownerId !== user.id) {         // autorizacion
    return { ok: false, error: 'not_allowed' }
  }
  await projects.remove(id)
  return { ok: true }
}

Pon cada lectura y escritura detrás de un Data Access Layer

El acceso a la base de datos vive en un Data Access Layer solo-servidor que devuelve DTO mínimos, nunca filas en crudo. La action llama a la capa, la capa impone la propiedad y da forma a la salida. Así un campo de más, un hash de contraseña o un flag interno, no acaba dentro de un componente de cliente. Y te da un único sitio donde añadir rate limiting a las operaciones costosas.

Devuelve un resultado tipado, nunca lances excepciones a través del límite

De cada action devolvemos una unión de resultados predecible, del tipo { ok: true, data } o { ok: false, error }, en lugar de lanzar excepciones. Una excepción cruza la red como un fallo opaco que la interfaz no sabe mostrar bien. Un resultado tipado deja que el formulario muestre el mensaje correcto y mantiene honestos al cliente y al servidor con TypeScript. Es la misma disciplina del RPC con tipos sobre Server Actions.

Deja que React 19 lleve el estado pendiente y la UX optimista

Las Server Actions se combinan con tres hooks de React 19. useActionState guarda el resultado devuelto y te da progressive enhancement, así un formulario funciona incluso con JavaScript desactivado. useFormStatus expone el estado pendiente de un botón de envío sin pasar props en cadena. useOptimistic actualiza la interfaz antes de que termine el viaje de red y luego reconcilia cuando llega el resultado real. La UI optimista la usamos en mutaciones frecuentes y de bajo riesgo (toggles, reordenaciones), no en las destructivas.

Elige la llamada de invalidación de caché correcta

Next.js 16 separa la invalidación de caché en herramientas distintas, y equivocarse es un fallo silencioso en producción. Usa revalidatePath para refrescar una ruta tras una mutación. Usa updateTag dentro de una action cuando el usuario deba leer su propia escritura en el mismo viaje: caduca el tag de inmediato y la siguiente petición espera datos frescos. Usa revalidateTag, que ahora pide un perfil cacheLife, para contenido que tolera stale-while-revalidate. Ajusta la llamada a si el usuario necesita ver el cambio ahora o más tarde.

Registra cada fallo que devuelves

Como devolvemos fallos tipados en lugar de lanzar excepciones, un intento no autorizado o un error de validación puede pasar en silencio si no lo registras. Por eso cada action registra la rama de fallo con el id de quien llama y el motivo, antes de devolverlo. Así la unión de resultados se convierte en un rastro de auditoría: muchos resultados not_allowed desde una misma cuenta son una señal que merece una alerta, no un aviso rojo que solo ve el usuario. Cuesta una línea por action y es la observabilidad más barata que añadirás en el trimestre.

Los patrones que evitamos

  • Usar una Server Action para leer datos. Las actions son solo POST y corren en serie, no son la herramienta para lecturas al estilo GET. Lee en un Server Component o en un Route Handler.
  • Aceptar subidas grandes directamente. Las peticiones de una action están limitadas a 1 MB por defecto. Puedes subir serverActions.bodySizeLimit, pero para archivos de más de unos megabytes usamos un Route Handler con streaming, o una subida directa al almacenamiento.
  • Fiarte del cliente para el id del usuario. El id de quien llama viene de la sesión en el servidor, nunca de un campo del formulario.
  • Una sola action gigante que hace cinco cosas. Las actions pequeñas y de un solo propósito son más fáciles de autorizar, probar y entender.
  • Ignorar la rotación de los id. Next.js rota los id de las Server Action al menos cada 14 días, así que un usuario con una pestaña vieja puede llamar a un id que ya no existe. En despliegues self-hosted con varias instancias fija una NEXT_SERVER_ACTIONS_ENCRYPTION_KEY estable y compartida entre instancias, y gestiona el caso "Failed to find Server Action" con un aviso para recargar.

Cómo se ve en la práctica

En un proyecto reciente, un formulario de ajustes llamaba a una sola action updateWorkspace. El esquema Zod rechazaba la entrada malformada, el Data Access Layer confirmaba que quien llamaba fuera admin de ese workspace, la action devolvía un resultado tipado y useActionState mostraba el error en línea sin recargar la página. Cuando la escritura salía bien, updateTag refrescaba el nombre del workspace allí donde apareciera en la misma respuesta. Todo el recorrido está en un archivo, sin ruta de API, y aguanta como un envío de formulario simple si un script no carga. Es el trato que nos gusta: menos fontanería, a condición de que cada action defienda su propia puerta.

En 2026 las Server Actions están listas para producción y sustituyen la mayoría de las rutas de API simples para mutaciones. No sustituyen el criterio. Valida, autoriza, devuelve un resultado tipado y mantén las lecturas fuera. Hazlo en cada action, no una sola vez en el borde, y el límite de red invisible deja de ser un riesgo.

Foto de Albert Stoynov en Unsplash

Preguntas frecuentes

¿Las Server Actions de Next.js son seguras por defecto?

En parte. Next.js te da una defensa a nivel de framework: compara el Origin de la petición con el Host y rechaza los POST cross-site, y solo acepta el método POST. Eso no es autenticación ni autorización. Cada action que exportas es un endpoint POST público alcanzable con una petición directa, así que debes añadir tú la autenticación y un control de propiedad o permisos dentro de cada action. La protección del framework es un suelo, no un muro.

¿Cuándo conviene usar un Route Handler en vez de una Server Action?

Usa un Route Handler para lecturas, para subidas de más de 1 MB y para todo lo que llame un cliente que no sea un navegador, como un webhook o una API pública. Las Server Actions son solo POST, corren en serie y están limitadas a 1 MB de cuerpo por defecto. Encajan en mutaciones lanzadas desde tu propia interfaz. Las lecturas van en Server Components o Route Handlers, y las subidas de archivos grandes van en un Route Handler con streaming o en un flujo directo al almacenamiento.

¿Las Server Actions funcionan sin JavaScript?

Sí, cuando las conectas mediante un formulario y useActionState. Como la action es un endpoint POST real, el navegador puede enviar el formulario y obtener una respuesta renderizada en el servidor aunque el script del cliente no cargue nunca. Eso es progressive enhancement, y es una razón para preferir el patrón formulario-más-useActionState frente a llamar a una action desde un handler onClick. La UI optimista con useOptimistic necesita JavaScript, así que es una mejora encima, no la base.

¿Por qué me sale 'Failed to find Server Action' tras desplegar?

Next.js identifica cada Server Action con un id incrustado en el build, y rota esos id al menos cada 14 días. Un usuario en una pestaña vieja de un despliegue anterior puede invocar un id que el nuevo build ya no conoce, y salta ese error. En hosting de una sola instancia basta con recargar. En despliegues self-hosted con varias instancias fija una NEXT_SERVER_ACTIONS_ENCRYPTION_KEY estable y compartida entre instancias para que los id sean consistentes, y captura el error para pedir al usuario que recargue.

Artículos relacionados

Studio

Empieza un proyecto.

Un partner único para todo el proyecto. Producción más rápida, tecnología moderna, costes reducidos.