Actions and forms
Actions are server functions you can call from the browser like an ordinary JavaScript function. Astro creates an address for them, validates the input against a zod schema and returns the result or an error. That is easier than writing an endpoint and parsing the request by hand. Actions need the server adapter from the on-demand rendering lesson.
Defining an action
All actions are declared in the file src/actions/index.ts. Create it:
// src/actions/index.ts
import { defineAction } from 'astro:actions';
import { z } from 'astro:schema';
const comments: { postId: string; author: string; text: string }[] = [];
export const server = {
addComment: defineAction({
accept: 'form',
input: z.object({
postId: z.string(),
author: z.string().min(2),
text: z.string().min(1).max(500),
}),
handler: async (input) => {
comments.push(input);
return { total: comments.length };
},
}),
};accept: 'form' says the data comes from an HTML form. input is the schema: a name of at least two characters, a text of 1 to 500. handler receives already validated data. Here comments live in server memory; a real blog saves them to a database.
A form on the post page
Add the form to src/pages/posts/[id].astro after the post text:
<form id="comment-form">
<input type="hidden" name="postId" value={post.id} />
<input name="author" placeholder="Your name" />
<textarea name="text" placeholder="Comment"></textarea>
<button>Send</button>
</form>
<p id="comment-status"></p>
<script>
import { actions, isInputError } from 'astro:actions';
const form = document.querySelector('#comment-form') as HTMLFormElement;
const status = document.querySelector('#comment-status') as HTMLElement;
form.addEventListener('submit', async (event) => {
event.preventDefault();
const { data, error } = await actions.addComment(new FormData(form));
if (isInputError(error)) {
status.textContent = error.fields.author?.join(', ') ?? 'Check the form fields';
} else if (error) {
status.textContent = 'Could not send the comment';
} else {
status.textContent = `Thank you! Comments: ${data.total}`;
form.reset();
}
});
</script>Open a post, enter the name "Oleg" and a text, click "Send", and "Thank you! Comments: 1" appears below the form. With a one-letter name you get an error message for the author field.
The call result
| Field | What it holds |
|---|---|
data | What handler returned, if everything went well |
error | An error: invalid input or a failure in handler |
error.fields | For validation errors, messages for each field |
To report an error yourself, handler throws an ActionError from astro:actions with a code such as NOT_FOUND.
A form without JavaScript
A form can also be sent without a script: the attributes action={actions.addComment} and method="POST" on the <form> tag. Then the page must be rendered on demand (export const prerender = false), and the result is read with Astro.getActionResult(actions.addComment).
Common mistake
People validate data only in the browser, with required and minlength attributes, and skip the input schema. A request to the action can be sent around the form, and anything ends up in the database. Validation in input runs on the server, so describe all data requirements there.