How to Use Astro Actions for Server-Side Data Mutations and API Endpoints

Astro Actions provide type-safe server-only functions that enable secure data mutations and API endpoints through a virtual module system, allowing client-side invocation via RPC calls or HTML forms without exposing server logic to the browser.

Astro Actions, available in the withastro/astro framework, solve the challenge of securely handling server-side mutations in modern web applications. This system lets you define type-safe server functions that can be invoked from the client or used internally within API routes and middleware, ensuring sensitive operations never execute in the browser.

How Astro Actions Work Under the Hood

Astro Actions leverage a virtual module system (astro:actions) that bridges client and server through a type-safe RPC layer. The implementation spans several key files in the Astro core:

When a client invokes actions.addToCart(), the client proxy creates a POST request to /_actions/addToCart. The Vite actions plugin ensures the server has access to the bundled actions code. On the server, parseRequestBody processes the JSON or FormData payload, validates it against the Zod schema defined in defineAction, and executes the handler within a safe server handler (safeServerHandler) that runs in an isolated ActionAPIContext. Results are serialized using devalue and returned to the client as a SafeResult object containing either data or error.

Defining Type-Safe Server Actions

To create a mutation endpoint, use defineAction exported from astro:actions. This function registers your handler at build time and enforces runtime input validation through the input property.

// src/actions/index.ts
import { defineAction, ActionError } from 'astro:actions';
import { z } from 'zod';

export const addToCart = defineAction({
  input: z.object({
    productId: z.string(),
    quantity: z.number().int().min(1),
  }),
  async handler({ productId, quantity }) {
    const result = await db.cart.add(productId, quantity);
    if (!result) throw new ActionError({ code: 'INTERNAL_SERVER_ERROR' });
    return { success: true };
  },
});

The input property accepts a Zod schema that automatically validates incoming requests. If validation fails, Astro returns a typed error to the client before your handler executes. The handler function runs exclusively on the server, allowing you to safely use database clients, environment variables, or file system APIs without exposing them to the browser.

Invoking Actions from Client Components

On the client, Astro generates a type-safe proxy that mirrors your server definitions. Import actions from astro:actions to access your defined mutations with full TypeScript support.

// src/components/AddToCartButton.jsx
import { actions } from 'astro:actions';

async function onClick() {
  const { data, error } = await actions.addToCart({
    productId: 'abc123',
    quantity: 2,
  });

  if (error) console.error(error);
  else console.log('Added to cart', data);
}

The client proxy, implemented in src/actions/runtime/client.ts, automatically constructs the correct POST request to /_actions/addToCart and handles the devalue-serialized response. The returned SafeResult type forces you to handle potential errors before accessing the data property, ensuring robust error handling.

Handling Actions via HTML Forms

Astro Actions support progressive enhancement through standard HTML forms. By posting directly to the RPC route, you can trigger mutations without any client-side JavaScript.

<form method="POST" action="/_actions/addToCart">
  <input type="hidden" name="_action" value="addToCart" />
  <input name="productId" value="abc123" />
  <input name="quantity" type="number" value="1" />
  <button type="submit">Add to cart</button>
</form>

When the form submits, Astro's server-side logic in src/actions/runtime/server.ts invokes parseRequestBody and formDataToObject to convert the FormData into the structured object expected by your Zod schema. This approach provides zero-JavaScript functionality while maintaining the same type-safe validation as RPC calls.

Server-to-Server Action Patterns

Within API routes or middleware, you can invoke actions directly without HTTP overhead using the server-side actions proxy, or inspect the current action context using getActionContext.

// src/pages/api/checkout.ts
import { actions, getActionContext } from 'astro:actions';
import type { APIContext } from 'astro';

export async function POST({ request }: APIContext) {
  const { action } = getActionContext({ request, locals: {} });
  
  const result = await actions.addToCart({ 
    productId: 'xyz', 
    quantity: 3 
  });
  
  return new Response(JSON.stringify(result));
}

The getActionContext helper, exported from src/actions/runtime/server.ts, extracts metadata about the current action request, including the action name and source. This enables middleware to intercept action results, modify headers, or implement custom logging across all mutations.

Summary

  • Astro Actions are defined using defineAction in src/actions/index.ts and provide type-safe server-only mutation handlers.
  • The system uses a virtual module (astro:actions) and Vite plugin to generate type-safe client proxies and server entry points.
  • Client invocation occurs via the actions proxy, which sends POST requests to /_actions/[action-name] and returns SafeResult objects.
  • HTML forms can post directly to /_actions/ endpoints for zero-JavaScript functionality, with automatic FormData parsing via parseRequestBody.
  • Server-side usage avoids HTTP round-trips by importing actions directly in API routes, while getActionContext enables middleware inspection of action metadata.

Frequently Asked Questions

How do Astro Actions differ from standard API routes?

Standard API routes require manual request parsing, validation, and response serialization. Astro Actions abstract this through defineAction, which automatically validates inputs against Zod schemas, handles errors via ActionError, and serializes responses using devalue, providing end-to-end type safety without boilerplate.

Can Astro Actions work without client-side JavaScript?

Yes. By posting HTML forms directly to the /_actions/[name] endpoint, you can trigger mutations without JavaScript. Astro parses the FormData automatically in src/actions/runtime/server.ts and validates it against your action's schema, supporting progressive enhancement patterns.

How does type safety work between the client and server?

The astro:actions virtual module generates TypeScript definitions based on your server-side defineAction exports. When you import actions on the client, TypeScript knows the exact parameter types and return shapes, ensuring compile-time safety that matches runtime validation.

What validation libraries are supported by Astro Actions?

Currently, Astro Actions integrates with Zod through the input property of defineAction. The system validates incoming requests against your Zod schema before executing the handler, throwing typed validation errors that the client proxy captures in the error property of the result object.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →