Actions
The problem Actions solve
Section titled “The problem Actions solve”To call server logic from the client the old way, you write an endpoint, invent a URL, fetch() it, stringify the body, guess at the response type, and validate the input by hand on the server. Actions collapse all of that into a single typed function: define it on the server, call it from the client with full type-safety and automatic validation.
Defining an action
Section titled “Defining an action”Actions live in src/actions/index.ts, exported as a server object. Each is a defineAction with an input schema (Zod) and a handler:
import { defineAction } from 'astro:actions';import { z } from 'astro/zod';
export const server = { createComment: defineAction({ input: z.object({ postId: z.string(), body: z.string().min(1).max(2000), }), handler: async (input, context) => { // input is fully typed and already validated against the schema. const comment = await db.comments.create({ postId: input.postId, body: input.body, userId: context.locals.user?.id, // per-request data from middleware }); return comment; // the return type flows to the client }, }),};Two things are automatic: the input is validated against the Zod schema before your handler runs, and the handler’s return type is inferred all the way to the caller — no manual typing of the response.
Calling an action from the client
Section titled “Calling an action from the client”Import actions from astro:actions and call it. Every call returns { data, error } — Astro never throws across the boundary:
import { actions, isInputError } from 'astro:actions';
const { data, error } = await actions.createComment({ postId: '42', body: 'Nice post!',});
if (isInputError(error)) { // Validation failed — error.fields has per-field messages. console.log(error.fields.body);} else if (data) { console.log('Created comment', data.id); // data is typed}sequenceDiagram
participant C as Client
participant A as Astro Action
participant H as handler
C->>A: actions.createComment(input)
A->>A: validate input against Zod schema
A->>H: run handler(input, context)
H-->>A: return value (typed)
A-->>C: { data, error } Forms and progressive enhancement
Section titled “Forms and progressive enhancement”An action can be wired directly to an HTML <form> by passing action.name — it works as a normal form POST even before JavaScript loads, then upgrades to a client call once hydrated. This is progressive enhancement: the form submits either way.
---import { actions } from 'astro:actions';---<form method="POST" action={actions.createComment}> <input type="hidden" name="postId" value="42" /> <textarea name="body"></textarea> <button type="submit">Comment</button></form>You can also invoke an action from a component script with Astro.callAction(actions.createComment, input) when you need its result during server render. The same validated handler runs in every case.