Przejdź do treści

Jak bezpiecznie obsługiwać formularze w Astro?

Poznaj Astro Actions. Dowiedz się, jak realizować bezpieczne mutacje danych i walidację Zod z pełnym wsparciem dla progressive enhancement.

Maciej Sala

Founder StriveLab

6 min czytaniaOpublikowano 29 maja 2026 (Aktualizacja 14 lipca 2026)

Załóżmy, że w Twoim projekcie istnieje już baza danych. Sama baza danych to jednak za mało, jeśli użytkownik nie ma możliwości zapisania komentarza, wysłania zgłoszenia czy zmiany ustawień. Ręczne tworzenie endpointów REST jest oczywiście możliwe, ale w wielu przypadkach generuje zbyt wiele zbędnego kodu. Właśnie tutaj z pomocą przychodzą Astro Actions, które znacząco skracają drogę od formularza do logiki serwerowej.

Jaki problem rozwiązują Astro Actions w formularzach i mutacjach danych

Wyobraź sobie typowy formularz komentarza. W podejściu z ręcznym endpointem przechodzisz przez cztery kroki, w których łatwo o błąd:

  1. Endpoint (src/pages/api/comment.ts), czyli sam wywołujesz request.json() albo request.formData().
  2. Walidacja, w której sprawdzasz pola ręcznie albo dokładasz Zod i obsługujesz wynik.
  3. Odpowiedź, czyli składasz Response z odpowiednim statusem HTTP i ciałem.
  4. Klient, czyli piszesz fetch('/api/comment'), obsługujesz błędy, a typy odpowiedzi przepisujesz ręcznie albo współdzielisz w osobnym module.

Bez wspólnego kontraktu między krokiem trzecim a czwartym TypeScript przestaje pomagać, bo serwer wie, co zwraca, ale klient już nie. Astro Actions automatycznie tworzą taki kontrakt i wystarczy, że raz zdefiniujesz logikę, a typ wyniku zostanie przekazany do wygenerowanego klienta.

Diagram
Jedna definicja akcji przyjmującej FormData, dwie drogi wywołania: RPC z wyspy interaktywnej i czysty formularz HTML — walidacja Zod zawsze przed funkcją handler.

Jak zdefiniować Astro Action w src/actions/index.ts

Wszystkie akcje eksportujesz jako obiekt server z pliku src/actions/index.ts, a każdą pojedynczą akcję opakowujesz w :

Code
// src/actions/index.ts
import { defineAction } from 'astro:actions'
import { z } from 'astro/zod'
import { db, Comment } from 'astro:db'
 
export const server = {
  addComment: defineAction({
    accept: 'form',
    input: z.object({
      postSlug: z.string().trim().min(1),
      author: z.string().trim().min(2, 'Imię musi mieć co najmniej 2 znaki'),
      body: z.string().trim().min(5).max(1000),
    }),
    handler: async (input) => {
      // Tu wejście jest już zwalidowane i otypowane
      const [comment] = await db
        .insert(Comment)
        .values({ ...input, approved: false })
        .returning()
 
      return { id: comment.id, status: 'pending' }
    },
  }),
}

Zachowanie akcji określają trzy pola:

  1. accept: 'form', czyli akcja przyjmuje FormData — dlatego tę samą definicję wywołasz z formularza HTML oraz z kodu klienckiego przekazującego FormData.
  2. input, czyli schemat , którym Astro waliduje dane jeszcze zanim wywoła handler. Jeśli wejście nie pasuje do schematu, handler w ogóle się nie uruchomi.
  3. handler, czyli logika serwerowa. Argument jest już zwalidowany i otypowany zgodnie ze schematem input, więc piszesz kod biznesowy bez defensywnych sprawdzeń.

Jak wywołać Astro Action z klienta jak zwykłą funkcję

Z komponentu klienckiego (React, Vue, Svelte albo <script>) importujesz akcje z i wywołujesz jak zwykłą funkcję. Ponieważ addComment ma accept: 'form', przekazujesz jej FormData:

Code
import { actions } from 'astro:actions'
import { useState, type FormEvent } from 'react'
 
export function CommentForm({ postSlug }: { postSlug: string }) {
  const [status, setStatus] = useState<string | null>(null)
 
  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault()
    const formData = new FormData(event.currentTarget)
    formData.set('postSlug', postSlug)
 
    const { data, error } = await actions.addComment(formData)
 
    if (error) {
      setStatus('Nie udało się dodać komentarza.')
      return
    }
    setStatus(`Komentarz przyjęty (status: ${data.status}).`)
  }
 
  return (
    <form onSubmit={onSubmit}>
      <input name="author" placeholder="Imię" required minLength={2} />
      <textarea name="body" placeholder="Komentarz" required minLength={5} />
      <button type="submit">Wyślij</button>
      {status && <p>{status}</p>}
    </form>
  )
}

data ma typ dokładnie odpowiadający temu, co zwraca handler ({ id, status }) i to bez ręcznego deklarowania interfejsu odpowiedzi. Zmiana wyniku akcji zostanie więc wykryta w komponencie przez TypeScript. Przy accept: 'form' wejściem po stronie klienta pozostaje jednak FormData; pełne typowanie obiektu wejściowego otrzymasz w akcji z domyślnym accept: 'json'.

Klasyczne formularze HTML w Astro Actions z accept: 'form'

Wywołanie RPC wymaga JavaScriptu po stronie klienta. Ta sama akcja z accept: 'form' obsłuży również formularz bez kodu klienckiego, gdy przypiszesz ją do atrybutu action formularza. Pełny przykład wraz z obsługą wyniku znajduje się poniżej.

Wynik formularza bez JavaScriptu: getActionResult i wzorzec PRG

Formularz działa, ale chcemy, by użytkownik widział, co się stało. Przy wysyłce bez JavaScriptu brakuje jednak funkcji setStatus. Od tego jest Astro.getActionResult(): po POST-cie strona renderuje się ponownie i we frontmatterze masz dostęp do wyniku akcji:

Code
---
import { actions, isInputError } from 'astro:actions'
 
const result = Astro.getActionResult(actions.addComment)
 
// Kod 303 wymusza przejście z POST do GET.
if (result && !result.error) {
  return Astro.redirect(`/blog/${Astro.params.slug}?comment=pending`, 303)
}
 
const inputErrors =
  result?.error && isInputError(result.error) ? result.error.fields : {}
---
 
<form method="POST" action={actions.addComment}>
  <input type="hidden" name="postSlug" value={Astro.params.slug} />
 
  <input
    name="author"
    required
    minlength="2"
    aria-invalid={!!inputErrors.author}
  />
  {inputErrors.author && <p class="error">{inputErrors.author[0]}</p>}
 
  <textarea
    name="body"
    required
    minlength="5"
    aria-invalid={!!inputErrors.body}
  ></textarea>
  {inputErrors.body && <p class="error">{inputErrors.body[0]}</p>}
 
  <button type="submit">Wyślij</button>
</form>

Po udanym wykonaniu akcji przekierowanie realizuje wzorzec POST-Redirect-GET. Kod 303 informuje przeglądarkę, że po żądaniu POST powinna pobrać stronę metodą GET, dzięki czemu odświeżenie nie spowoduje ponownego wysłania komentarza. Błędy walidacji pozostają na stronie, a error.fields z isInputError przypisujesz do odpowiednich pól, analogicznie jak w wariancie RPC.

Akcję można uruchomić również z poziomu kodu serwerowego strony za pomocą Astro.callAction(actions.addComment, formData). W przypadku akcji z accept: 'form' drugim argumentem nadal musi być FormData. W endpoincie serwerowym odpowiednikiem tej funkcji jest context.callAction().

Obsługa błędów w Astro Actions: { data, error } lub .orThrow()

Standardowe wywołanie Astro Action zwraca obiekt { data, error }, dzięki czemu obsługujesz niepowodzenie jawnie, a jeśli w danym miejscu wygodniejszy będzie try/catch, możesz wywołać actions.addComment.orThrow(formData). Ten wariant zwraca bezpośrednio dane albo rzuca wyjątek.

Błędy dzielą się na dwie kategorie:

Code
import { actions, isInputError } from 'astro:actions'
 
const { error } = await actions.addComment(formData)
 
if (error) {
  if (isInputError(error)) {
    // Błąd walidacji Zod — odczytasz go dla każdego pola.
    const fieldErrors = error.fields
    console.log(fieldErrors.author) // np. ['Imię musi mieć min. 2 znaki']
  } else {
    // Błąd biznesowy rzucony z handlera (ActionError)
    console.log(error.code) // np. 'UNAUTHORIZED'
  }
}
  • rozpoznaje błąd walidacji Zod. error.fields zawiera komunikaty dla poszczególnych pól, gotowe do wyświetlenia przy kontrolkach formularza.
  • rzucasz go z handlera dla błędów biznesowych, z kodem statusu zamiast surowego HTTP:
Code
import { defineAction, ActionError } from 'astro:actions'
 
handler: async (_input, { locals }) => {
  const user = locals.user
  if (!user) {
    throw new ActionError({
      code: 'UNAUTHORIZED',
      message: 'Musisz być zalogowany, żeby komentować.',
    })
  }
  // ...
}

Astro Actions czy endpointy serwerowe: kiedy wybrać które rozwiązanie

Astro Actions nie zastępują endpointów całkowicie, o czym poniżej.

PotrzebaWybór
Mutacja danych z własnej aplikacji (formularz, przycisk, akcja użytkownika)Astro Actions — typowanie, walidacja, mniej kodu
Publiczne API REST dla zewnętrznych klientówEndpoint serwerowy — pełna kontrola nad ścieżką, metodą i nagłówkami
Webhook przyjmujący żądania spoza Twojej aplikacjiEndpoint serwerowy — Actions zakładają własnego klienta
Zwracanie pliku, strumienia lub własnej odpowiedzi HTTPEndpoint serwerowy

Pamiętaj, by wewnątrz własnej aplikacji wybierać Actions, a dla klientów zewnętrznych — endpoint serwerowy.

Bezpieczeństwo Astro Actions i dobre praktyki wdrożenia

  • Walidacja nie jest autoryzacją. input Zod sprawdza kształt danych, ale nie uprawnienia. Sprawdzenie, czy użytkownik może wykonać akcję, robisz w handlerze przez context.locals i ActionError.
  • Nie ufaj polom ukrytym. postSlug z <input type="hidden"> może zostać podmieniony. Waliduj, że odnosi się do realnego, dozwolonego zasobu.
  • Obsłuż error po stronie wywołującego. Przy standardowym wywołaniu pominięcie sprawdzenia error oznacza ciche niepowodzenie. Używając w sposób przemyślany .orThrow(), przechwycisz wyjątek wtedy, kiedy trzeba.
  • Ogranicz liczbę zgłoszeń. Publiczne akcje, np. komentarze lub formularze kontaktowe, potrzebują limitu wywołań egzekwowanych w funkcji handler lub w middleware.
  • Nie wyłączaj ochrony origin bez konkretnego powodu. security.checkOrigin domyślnie chroni renderowane na żądanie formularze przed CSRF dla obsługiwanych typów treści. Nie obejmuje jednak każdego możliwego klienta i nie zastąpi autoryzacji.

Miejsce Astro Actions w nowoczesnym stosie Astro

Astro Actions spinają warstwę danych z interfejsem:

  • Mutacja danych w bazie (np. libSQL/Turso przez Drizzle) odbywa się bezpośrednio: od formularza, poprzez akcję, do db.insert, bez konieczności użycia pośredniego endpointu.
  • Wywołujesz je z wysp interaktywnych (React/Vue/Svelte) albo z czystego formularza HTML.
  • Middleware może odczytać sesję i umieścić użytkownika w context.locals, ale uprawnienia do wykonania mutacji sprawdza sam handler. Budowę tej warstwy opisuję w artykule o middleware i autoryzacji w Astro.
Ultraszybkie projekty, łączące lekkość ze skalowalnością.
Astro

Często zadawane pytania

Czy Astro Actions są stabilne, czy eksperymentalne?

Stabilne i gotowe do produkcji. Trafiły do Astro w wersji 4.15 i w Astro 5 oraz 6 działają bez żadnej flagi eksperymentalnej. Definiujesz je w pliku src/actions/index.ts, ale do wdrożenia nadal potrzebny będzie adapter zapewniający środowisko serwerowe.

Czym Astro Actions różnią się od endpointów API?

Endpoint to ręcznie pisana funkcja obsługująca HTTP: sam odczytujesz ciało żądania, walidujesz dane, zwracasz Response i po stronie klienta wywołujesz fetch. Action to funkcja serwerowa z walidacją Zod wbudowaną w definicję. Wywołujesz ją przez wygenerowanego klienta, który zna typ wyniku, bez ręcznego składania warstwy komunikacji HTTP.

Czy Astro Actions wymagają trybu serwerowego?

Tak. Projekt potrzebuje adaptera i możliwości uruchamiania funkcji backendowych. Strona wywołująca akcję przez JavaScript może pozostać prerenderowana, ale formularz HTML z accept: form wymaga strony renderowanej na żądanie (output: server albo prerender = false).

Jak walidować dane wejściowe w Astro Actions?

Przez pole input z schematem Zod w definicji akcji. Astro waliduje wejście automatycznie przed uruchomieniem funkcji handler. Jeśli dane nie pasują do schematu, handler się nie uruchomi, a klient dostanie błąd walidacji, który rozpoznasz funkcją isInputError i odczytasz dla każdego pola przez error.fields.

Jak pokazać wynik akcji przy formularzu bez JavaScriptu?

Przez Astro.getActionResult(actions.nazwa) we frontmatterze strony — po wysyłce formularza POST zwraca { data, error } tej akcji. Błędy walidacji odczytasz przez isInputError i pokażesz przy polach, a po sukcesie wykonaj Astro.redirect na tę samą lub inną stronę (wzorzec POST-Redirect-GET), żeby odświeżenie strony nie wysłało formularza ponownie.

Jak Astro Actions obsługują błędy?

Standardowe wywołanie zwraca obiekt { data, error }. Błędy walidacji rozpoznasz przez isInputError(error). W handlerze rzucasz ActionError z kodem statusu, np. UNAUTHORIZED lub NOT_FOUND, który trafia do klienta jako error.code. Jeśli wolisz wyjątki, użyj actions.nazwa.orThrow().

O autorze

Maciej Sala

Maciej Sala — Product Manager i Frontend Developer z bogatym doświadczeniem w marketingu internetowym oraz SEO. Na co dzień pracuje z Reactem, Next.js i TypeScriptem, a ostatnio także z Astro i narzędziami do automatyzacji procesów AI. Sprawnie łączy perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat był związany z branżą gier wideo jako project manager i game designer. Absolwent historii na Uniwersytecie Jagiellońskim oraz studiów podyplomowych z marketingu internetowego na AGH w Krakowie. Po godzinach trenuje na siłowni, maluje figurki i rozwijam własne projekty.

Pomagam przekładać takie tematy na konkretne wdrożenia w frontendzie, SEO, analityce i procesie produktowym.

Skontaktuj się ze mną

Biblioteka wiedzy na temat Astro

Czytaj dalej

Zobacz więcej wpisów
Astro.js vs Next.js w 2026 — kompleksowe porównanie frameworków

Artykuł nie jest manifestem o porzuceniu jednej technologii na rzecz drugiej, lecz bardziej analizą, w której staram się pokazać obiektywnie, w których obszarach Astro dostarcza bezdyskusyjną przewagę wydajnościową, a w jakich scenariuszach Next.js pozostaje niezastąpionym wyborem.

Maciej Sala

Maciej Sala

Founder StriveLab

React 19 Actions — formularz bez onSubmit, useOptimistic i useActionState w praktyce

React 19 pozwala przekazać funkcję bezpośrednio do atrybutu action elementu . Dzięki integracji z Actions, useActionState , useFormStatus i useOptimistic możesz ograniczyć ręczną obsługę onSubmit , pending state oraz optymistycznych aktualizacji , zachowując kontrolę nad błędami i stanem trwałym.

Maciej Sala

Maciej Sala

Founder StriveLab

Pierwszy projekt w Astro — od npm create astro do wdrożenia w 15 minut

W Astro jedna komenda stawia projekt, serwer lokalny i pierwszy widok, więc nie musisz spędzać połowy dnia od godzin konfiguracji. Domyślnie dostajesz szybki, statyczny HTML, a JavaScript dokładasz dopiero tam, gdzie faktycznie jest potrzebny. Przejdziemy od npm create astro do wdrożonej strony i po drodze wyjaśnię, jakie potrzeby spełnia projekt stworzony w Astro.

Maciej Sala

Maciej Sala

Founder StriveLab