Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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

FieldWhat it holds
dataWhat handler returned, if everything went well
errorAn error: invalid input or a failure in handler
error.fieldsFor 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.

Thanks for reading!

Donate to the site — we work hard for you.