TS
#astro#actions#zod#forms#validation#server

Astro Actions: форма с валидацией Zod

Определение Action с accept: form, валидация полей через Zod (astro/zod), вызов из HTML-формы и Astro.getActionResult().

Как использовать

  1. Добавь Action в src/actions/index.ts с accept: form и input через z.object().
  2. На странице используй action={actions.contact} в форме и Astro.getActionResult() для отображения результата или ошибок.
  3. Убедись, что страница с формой рендерится on-demand (prerender: false или output server/hybrid).

При обработке форм в Astro в режиме SSR или hybrid часто нужна типобезопасная валидация на сервере и единообразный вывод ошибок по полям. Без этого данные приходят «как есть», дублирование правил на клиенте и сервере приводит к рассинхрону, а пользователь видит общее сообщение об ошибке вместо привязки к конкретному полю. Симптомы: форма отправляется с некорректными данными, ошибки валидации не отображаются у полей, либо страница с формой отдаёт 404 или не вызывает action при статической сборке. Ниже — готовое решение через Astro Actions и Zod: одна схема валидации, вывод ошибок по полям и проверка запроса через curl.

Решение

Обработка форм в Astro 5 через Actions: типобезопасность и валидация на сервере. Zod импортируется из astro/zod, Actions — из astro:actions. Для вызова action через атрибут формы страница должна быть on-demand (export const prerender = false или output: "server" / "hybrid").

Определение Action (docs.astro.build — Actions):

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

export const server = {
  contact: defineAction({
    accept: "form",
    input: z.object({
      name: z.string().min(2, "Имя минимум 2 символа"),
      email: z.string().email("Некорректный email"),
      message: z.string().min(10, "Сообщение минимум 10 символов"),
    }),
    handler: async (input) => {
      const { name, email, message } = input;
      // отправка email, запись в БД и т.д.
      return {
        success: true,
        message: "Спасибо за обращение! Мы свяжемся с вами.",
      };
    },
  }),
};

Форма и результат на странице (Call actions from HTML form action):

---
// src/pages/contacts.astro
import { actions } from "astro:actions";

const result = Astro.getActionResult(actions.contact);
---

{result?.data?.success && (
  <p class="success">{result.data.message}</p>
)}
{result?.error && (
  <p class="error">{result.error.message}</p>
)}

<form method="POST" action={actions.contact}>
  <label for="name">Имя</label>
  <input type="text" id="name" name="name" required />
  {result?.error?.fields?.name && (
    <span class="field-error">
      {Array.isArray(result.error.fields.name) ? result.error.fields.name[0] : result.error.fields.name}
    </span>
  )}
  <label for="email">Email</label>
  <input type="email" id="email" name="email" required />
  <label for="message">Сообщение</label>
  <textarea id="message" name="message" rows="5" required></textarea>
  <button type="submit">Отправить</button>
</form>

Вызов Action из клиентского скрипта (прогрессивное улучшение):

import { actions, isInputError } from "astro:actions";

const form = document.querySelector("form");
form?.addEventListener("submit", async (e) => {
  e.preventDefault();
  const formData = new FormData(form);
  const { data, error } = await actions.contact(formData);
  if (isInputError(error)) {
    // ошибки полей в error.fields
    return;
  }
  if (error) {
    // серверная ошибка
    return;
  }
  if (data) {
    // успех: data.message и т.д.
  }
});
  • Все Actions экспортируются из объекта server в src/actions/index.ts.
  • При accept: "form" handler получает уже распарсенный и провалидированный объект (не сырой FormData).
  • Ошибки валидации содержат error.fields (сообщения по имени поля); для проверки используйте isInputError(error).

Проверка

Убедиться, что форма и валидация работают:

  1. Запуск dev-сервера (страница с формой должна быть on-demand):
pnpm dev

Открой страницу с формой (например /contacts), отправь форму с пустым или неверным email — под полем должно появиться сообщение вида «Некорректный email».

  1. Диагностика ответа action через curl (проверка, что сервер возвращает структуру ошибки):
curl -X POST "http://localhost:4321/contacts" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "name=A&email=invalid&message=short"

В ответе ожидаются данные в формате, который Astro использует для getActionResult (например, редирект с cookie или тело с полем error и error.fields в зависимости от реализации). Успешная отправка с валидными данными должна вернуть редирект или страницу с сообщением об успехе.

  1. Проверка типа ошибки в коде: если обрабатываешь action в клиентском скрипте, используй isInputError(error) — тогда можно безопасно выводить error.fields по полям.

Типичные ошибки

  • Страница с формой отдаёт 404 или action не срабатывает — страница собрана статически. Добавь в frontmatter страницы export const prerender = false или используй output: "server" / "hybrid" в astro.config.mjs.
  • Ошибки валидации не показываются у полей — не выводишь result?.error?.fields?.name (и остальные поля). Убедись, что имена полей в форме совпадают с ключами в z.object().
  • В клиентском скрипте падает при обращении к error.fields — сначала проверь isInputError(error); у серверных ошибок структура другая.

Где применять

  • Разработка: локальный pnpm dev с режимом server или hybrid.
  • Продакшен: хостинг с поддержкой Node (Vercel, Netlify, свой сервер) и выбранным адаптером деплоя. Страница с формой не должна быть пререндерена.

Связанные сниппеты: API Route (endpoint) с динамическим путём, Подключение React в островке, Статический вывод по умолчанию.

⚠️ Страница с формой, вызывающей action через action={actions.contact}, должна быть отрендерена on-demand. В статическом режиме добавь в frontmatter страницы export const prerender = false или используй output: "server" / "hybrid".