Ir al contenido
angel.library

Server Actions

Para un formulario, un endpoint a mano implica repetir siempre lo mismo: parsear el body, validarlo, tipar la respuesta, manejar el error de red en el cliente. Las Actions empaquetan las tres cosas en una sola función: defines el schema de entrada una vez con Zod, y tanto el cliente como el servidor comparten ese tipo automáticamente — sin escribir el fetch a mano ni duplicar la validación en el frontend.

Viven en src/actions/index.ts, exportadas dentro de un objeto server.

src/actions/index.ts
import { z } from "astro/zod"
import { defineAction } from "astro:actions"
export const server = {
crearComentario: defineAction({
input: z.object({
texto: z.string().min(1),
postId: z.string()
}),
handler: async (input) => {
// input ya está validado y tipado aquí
return { id: crypto.randomUUID(), ...input }
}
})
}
import { actions } from "astro:actions"
const { data, error } = await actions.crearComentario({
texto: "Buen artículo",
postId: "post-1"
})
if (!error) console.log(data.id)

Con accept: 'form' en la action, un <form> normal (sin JS) apuntando a actions.nombre funciona server-side. Con JS activo, Astro intercepta el submit y lo maneja sin recargar la página.

crearComentario: defineAction({
accept: "form",
input: z.object({ texto: z.string().min(1), postId: z.string() }),
handler: async (input) => {
/* ... */
}
})
---
import { actions } from "astro:actions"
---
<form
method="POST"
action={actions.crearComentario}
>
<textarea
name="texto"
required
></textarea>
<input
type="hidden"
name="postId"
value={post.id}
/>
<button>Comentar</button>
</form>

Para el caso sin JS (o SSR directo), Astro.getActionResult() lee el resultado del submit en el render siguiente.

---
import { actions } from "astro:actions"
const result = Astro.getActionResult(actions.crearComentario)
if (result && !result.error) {
return Astro.redirect(`/post/${result.data.postId}`)
}
---

ActionError para errores controlados (permisos, no encontrado); isInputError para distinguir errores de validación de Zod y leer qué campo falló.

import { ActionError, isInputError } from "astro:actions"
crearComentario: defineAction({
input: z.object({ texto: z.string().min(1), postId: z.string() }),
handler: async (input, context) => {
if (!context.locals.user) {
throw new ActionError({ code: "UNAUTHORIZED", message: "Inicia sesión" })
}
return { id: crypto.randomUUID() }
}
})
const { data, error } = await actions.crearComentario(input)
if (isInputError(error)) {
console.log(error.fields.texto) // ["String must contain at least 1 character(s)"]
} else if (error?.code === "UNAUTHORIZED") {
mostrarLogin()
}
APIUso
defineAction({ input, handler })Definir una action con validación Zod
accept: 'form'Aceptar submits de <form> además de JSON
actions.nombre(input)Llamar la action desde un componente cliente
Astro.getActionResult(actions.nombre)Leer el resultado server-side después del submit
ActionErrorLanzar un error controlado con code y message
isInputError(error)Detectar errores de validación y leer error.fields
  • La autorización va dentro del handler (context.locals), no antes — las actions no pasan por las rutas de página, así que un middleware de auth genérico no las cubre automáticamente a menos que uses getActionContext() ahí.
  • input con Zod valida antes de que el handler corra: un input inválido nunca llega al handler, sale como isInputError.
  • Con accept: 'form' y JS desactivado, el navegador hace un submit normal y Astro re-renderiza la página — Astro.getActionResult() es la única forma de leer ese resultado en ese caso.