The one idea
An Action is a type-safe server function: call it like a local
async fn from the client via the actions object, and get a
typed { data, error } result with Zod input validation for free.
A raw API endpoint is a src/pages/api/*.ts handler that returns a
Response — you hand-roll fetch, JSON, headers, status, and types.
Actions are defined with defineAction() inside a server object in
src/actions/index.ts, and called through import { actions } from 'astro:actions'.
Added in astro@4.15 (preview 4.8/4.10); both need on-demand (SSR) rendering.
Side by side — the same "greeting" as an Action and as an endpoint
src/actions/index.ts Action
import { defineAction } from 'astro:actions'; import { z } from 'astro/zod'; export const server = { getGreeting: defineAction({ input: z.object({ name: z.string().min(1), }), handler: async (input) => ({ message: `Hello, ${input.name}!`, chars: input.name.length, }), }), };
// client (typed — no fetch, no JSON.parse): import { actions, isInputError } from 'astro:actions'; const { data, error } = await actions.getGreeting({ name: 'Houston' }); if (isInputError(error)) { console.log(error.fields.name); // ['String must contain at least 1 character(s)'] } // data.message <-- typed: string
src/pages/api/echo.ts API endpoint
import type { APIRoute } from 'astro'; export const POST = (async ({ request }) => { const body = await request.json(); const name = body.name ?? ''; return new Response(JSON.stringify({ message: `Hello, ${name}!`, chars: name.length, }), { status: 200, headers: { 'Content-Type': 'application/json' }, }); }) satisfies APIRoute;
// client (untyped — you fetch, check res.ok, parse JSON): const res = await fetch('/api/echo', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Houston' }), }); if (!res.ok) { /* handle status yourself */ } const data = await res.json(); // <-- untyped: any
Try it — call the same input two ways
Type a name (or use a chip) and call it as an Action (typed + validated by a
tiny inline Zod-like validator) or as a raw endpoint (no validation — echoes anything).
The result + the request/response flow update live.
Request / response flow
The Action lane validates input with Zod on the server and hands the client a typed
{ data, error }. The endpoint lane returns a Response the client must
await res.json() — validation and types are on you.
Comparison — when to reach for which
defineAction for type-safe server fns,
src/pages/api/*.ts for raw endpoints. Cross-refs:
🔗 astro_react_integration (islands call actions) ·
🔗 astro_rendering_modes (actions need on-demand output).