Documentation Index
Fetch the complete documentation index at: https://mintlify.com/withastro/astro/llms.txt
Use this file to discover all available pages before exploring further.
Astro Actions provide type-safe server functions that can be called from client code or Astro components.
Importing
import { defineAction } from 'astro:actions';
import { z } from 'astro:schema';
defineAction
Define a server-side action that can be called from the client or server.
defineAction(config: ActionConfig): ActionClient
Action configuration object.Zod schema to validate input data. If accept is 'form', defaults to FormData schema.import { defineAction } from 'astro:actions';
import { z } from 'astro:schema';
export const server = {
getUser: defineAction({
input: z.object({
userId: z.string(),
}),
handler: async (input) => {
return { name: 'John Doe', id: input.userId };
},
}),
};
Type of input the action accepts. Use 'form' for form submissions or 'json' for JSON payloads.Default: 'json'defineAction({
accept: 'form',
input: z.object({
email: z.string().email(),
message: z.string(),
}),
handler: async (input) => {
// input is parsed from FormData
return { success: true };
},
})
config.handler
(input, context) => Promise<Output>
required
The server function that executes the action.Parameters:Parsed and validated input data matching your schema.
Server context with request information.The original request object.
handler: async ({ userId }, context) => {
const user = await db.users.findOne({ id: userId });
if (!user) {
throw new Error('User not found');
}
return user;
}
Example
// src/actions/index.ts
import { defineAction } from 'astro:actions';
import { z } from 'astro:schema';
export const server = {
createPost: defineAction({
accept: 'form',
input: z.object({
title: z.string(),
content: z.string(),
published: z.boolean().optional(),
}),
handler: async (input, context) => {
const user = context.locals.user;
if (!user) {
throw new Error('Not authenticated');
}
const post = await db.posts.create({
...input,
authorId: user.id,
});
return { id: post.id };
},
}),
getPost: defineAction({
input: z.object({
postId: z.string(),
}),
handler: async ({ postId }) => {
const post = await db.posts.findOne({ id: postId });
if (!post) {
throw new Error('Post not found');
}
return post;
},
}),
};
Calling Actions
From Client-Side JavaScript
import { actions } from 'astro:actions';
const result = await actions.getPost({ postId: '123' });
if (result.error) {
console.error(result.error);
} else {
console.log(result.data);
}
---
import { actions } from 'astro:actions';
---
<form method="POST" action={actions.createPost}>
<input type="text" name="title" required />
<textarea name="content" required></textarea>
<button type="submit">Create Post</button>
</form>
From Astro Components
Use Astro.callAction() to call actions server-side:
---
import { actions } from 'astro:actions';
const result = await Astro.callAction(actions.getPost, {
postId: Astro.params.id
});
if (result.error) {
return Astro.redirect('/404');
}
const post = result.data;
---
<h1>{post.title}</h1>
<div>{post.content}</div>
Use Astro.getActionResult() to get results from form submissions:
---
import { actions } from 'astro:actions';
const result = Astro.getActionResult(actions.createPost);
---
{result?.error && (
<p class="error">{result.error.message}</p>
)}
{result?.data && (
<p class="success">Post created with ID: {result.data.id}</p>
)}
<form method="POST" action={actions.createPost}>
<input type="text" name="title" required />
<textarea name="content" required></textarea>
<button type="submit">Create Post</button>
</form>
Error Handling
Actions return a SafeResult object with either data or error:
type SafeResult<TOutput> =
| { data: TOutput; error: undefined }
| { data: undefined; error: ActionError };
Throwing Errors
Throw errors in your handler to return error responses:
import { defineAction, ActionError } from 'astro:actions';
import { z } from 'astro:schema';
export const server = {
deletePost: defineAction({
input: z.object({ postId: z.string() }),
handler: async ({ postId }, context) => {
const user = context.locals.user;
if (!user) {
throw new ActionError({
code: 'UNAUTHORIZED',
message: 'You must be logged in to delete posts',
});
}
const post = await db.posts.findOne({ id: postId });
if (!post) {
throw new ActionError({
code: 'NOT_FOUND',
message: 'Post not found',
});
}
if (post.authorId !== user.id) {
throw new ActionError({
code: 'FORBIDDEN',
message: 'You can only delete your own posts',
});
}
await db.posts.delete({ id: postId });
return { success: true };
},
}),
};
Error Codes
HTTP-like error codes:
'BAD_REQUEST' - Invalid input (400)
'UNAUTHORIZED' - Authentication required (401)
'FORBIDDEN' - Insufficient permissions (403)
'NOT_FOUND' - Resource not found (404)
'TIMEOUT' - Request timeout (408)
'CONFLICT' - Resource conflict (409)
'PRECONDITION_FAILED' - Precondition not met (412)
'PAYLOAD_TOO_LARGE' - Request too large (413)
'UNSUPPORTED_MEDIA_TYPE' - Invalid content type (415)
'UNPROCESSABLE_CONTENT' - Validation failed (422)
'TOO_MANY_REQUESTS' - Rate limit exceeded (429)
'CLIENT_CLOSED_REQUEST' - Client cancelled (499)
'INTERNAL_SERVER_ERROR' - Server error (500)
'NOT_IMPLEMENTED' - Not implemented (501)
'BAD_GATEWAY' - Bad gateway (502)
'SERVICE_UNAVAILABLE' - Service unavailable (503)
'GATEWAY_TIMEOUT' - Gateway timeout (504)
Input is automatically validated using the provided Zod schema:
export const server = {
updateProfile: defineAction({
accept: 'form',
input: z.object({
name: z.string().min(2).max(50),
email: z.string().email(),
age: z.number().int().min(18).optional(),
}),
handler: async (input) => {
// input is fully typed and validated
return { success: true };
},
}),
};
Validation errors are returned automatically:
const result = await actions.updateProfile({
name: 'A', // Too short
email: 'invalid', // Invalid email
});
if (result.error) {
console.log(result.error.fields); // Validation errors by field
}
orThrow
Call .orThrow() to throw on errors instead of returning a result object:
try {
const post = await actions.getPost.orThrow({ postId: '123' });
// post is TOutput, not SafeResult
console.log(post.title);
} catch (error) {
console.error(error);
}
This is useful for server-side code where you want exceptions instead of result objects.