← Semua pembelajaran / Astro Nol → Portal Berita
Fase 1 · Routing, Layout & Model Rendering

Actions — form dan mutasi yang tervalidasi

Di CI3, validasi adalah sesuatu yang kamu ingat untuk menulis. Di Actions, ia bagian dari definisi fungsinya — dan kode yang melewatkannya tidak akan dikompilasi.

Sumber asli docs.astro.build Resmi Rangkuman ~9 menit baca

Intisari

  • Action adalah fungsi server yang dipanggil dari klien dengan tipe yang terjaga di kedua sisi.
  • input divalidasi Zod sebelum handler jalan. Tidak ada jalan masuk yang melewatinya.
  • accept: "form" membuatnya bekerja dengan <form> biasa — termasuk saat JavaScript mati.
  • Error dikembalikan sebagai nilai ({ data, error }), bukan dilempar. Kamu dipaksa menanganinya.
  • Astro memasang perlindungan CSRF untuk action berbasis form; endpoint .ts buatanmu sendiri tidak dapat itu.

Mendefinisikan action

// src/actions/index.ts
import { defineAction, ActionError } from "astro:actions";
import { z } from "astro:schema";
import { daftarkanNewsletter } from "@/lib/newsletter";

export const server = {
  langganNewsletter: defineAction({
    accept: "form",
    input: z.object({
      email: z.string().email("Format email tidak valid"),
      kategori: z.enum(["ekonomi", "pasar", "teknologi"]).default("ekonomi"),
      setuju: z.literal("on", { message: "Kamu harus menyetujui ketentuan" }),
    }),
    handler: async ({ email, kategori }, context) => {
      const ip = context.locals.ipPembaca;

      if (await terlaluSering(ip)) {
        throw new ActionError({
          code: "TOO_MANY_REQUESTS",
          message: "Terlalu banyak percobaan. Coba lagi beberapa menit lagi.",
        });
      }

      await daftarkanNewsletter(email, kategori);
      return { pesan: "Cek emailmu untuk konfirmasi." };
    },
  }),
};

Saat handler berjalan, email sudah dipastikan string berformat email dan kategori sudah dipastikan salah satu dari tiga nilai itu. Tidak ada cabang eksekusi yang melewati validasi — bukan karena kamu disiplin, tapi karena tidak ada jalan lain masuk ke sana.

Bandingkan dengan CI3

public function langganan()
{
    $email = $this->input->post('email');
    $kategori = $this->input->post('kategori');

    // Semuanya opsional. Semuanya bisa lupa.
    // $email bisa null, array, atau string 10 MB.
    // $kategori bisa string apa pun yang langsung masuk ke query.

    $this->newsletter_model->daftar($email, $kategori);
    redirect('/terima-kasih');
}

Di CI3, $this->input->post('kategori') bisa berisi apa saja — termasuk array, yang akan membuat perbandingan string berperilaku aneh, atau nilai yang tidak ada di daftar yang kamu harapkan. z.enum([...]) menutup seluruh kelas masalah itu dalam satu baris.

Memakainya dari form biasa

---
import { actions } from "astro:actions";

const hasil = Astro.getActionResult(actions.langganNewsletter);
---
{hasil?.error && (
  <div class="galat" role="alert">{hasil.error.message}</div>
)}

{hasil?.data && (
  <div class="sukses" role="status">{hasil.data.pesan}</div>
)}

<form method="POST" action={actions.langganNewsletter}>
  <label>Email <input type="email" name="email" required /></label>

  <label>Kategori
    <select name="kategori">
      <option value="ekonomi">Ekonomi</option>
      <option value="pasar">Pasar</option>
      <option value="teknologi">Teknologi</option>
    </select>
  </label>

  <label><input type="checkbox" name="setuju" /> Saya setuju</label>

  <button type="submit">Berlangganan</button>
</form>

Form ini bekerja tanpa JavaScript sama sekali. action={actions.…} menghasilkan URL biasa, dan browser mengirim POST seperti form HTML tahun 1997. Untuk portal berita ini penting: sebagian pembacamu memakai koneksi lambat, peramban dalam aplikasi media sosial, atau perangkat lama di mana JavaScript gagal dimuat. Form yang hanya bekerja setelah bundel JS berhasil diunduh adalah form yang kehilangan pendaftar.

Memakainya dari island Vue

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

async function kirim() {
  const { data, error } = await actions.langganNewsletter({
    email: email.value,
    kategori: "ekonomi",
    setuju: "on",
  });

  if (isInputError(error)) {
    // Error validasi per field
    galat.value = error.fields.email?.[0] ?? "Periksa lagi isianmu";
    return;
  }
  if (error) {
    galat.value = error.message;
    return;
  }

  pesan.value = data.pesan;
}

data di sini bertipe — TypeScript tahu ia punya field pesan, karena tipenya disimpulkan dari nilai kembalian handler. Tidak ada as, tidak ada tebakan. Ini keuntungan yang tidak bisa diberikan endpoint REST buatan sendiri tanpa alat pembangkit tipe terpisah.

Error sebagai nilai, bukan lemparan

const { data, error } = await actions.langganNewsletter(input);

Kamu tidak bisa mengakses data tanpa melihat bahwa error ada — TypeScript menandainya sebagai mungkin undefined. Ini kebalikan dari try/catch yang mudah dilupakan. Kode yang mengabaikan error tidak lolos astro check.

Kode ActionErrorStatus HTTPPakai untuk
BAD_REQUEST400Input tidak masuk akal secara bisnis
UNAUTHORIZED401Belum login
FORBIDDEN403Login, tapi bukan haknya
NOT_FOUND404Sumber daya tidak ada
CONFLICT409Email sudah terdaftar
TOO_MANY_REQUESTS429Rate limit

Kapan Actions, kapan endpoint .ts

Pakai Actions kalauPakai endpoint .ts kalau
Dipanggil dari halamanmu sendiriDipanggil pihak luar — webhook Midtrans
Ingin tipe otomatis di klienBentuk request ditentukan pihak lain
Ingin form yang jalan tanpa JSButuh kendali penuh atas status dan header
Ingin CSRF ditangani untukmuButuh membaca raw body untuk verifikasi tanda tangan

Webhook Midtrans wajib endpoint .ts, bukan action. Verifikasi tanda tangannya dihitung dari body mentah, dan Actions sudah mem-parsing body sebelum kodemu melihatnya. Selain itu Midtrans tidak mengirim token CSRF-mu. Ini dibahas tuntas di Fase 6 — untuk sekarang, cukup ingat batasnya.

CSRF: yang kamu dapat dan yang tidak

Astro memeriksa header Origin untuk action berbasis form. Itu menutup kasus umum CSRF tanpa kamu menulis satu baris pun. Tapi perlindungan itu tidak berlaku untuk endpoint .ts buatanmu — di sana kamu bertanggung jawab sendiri. Kalau kamu menulis src/pages/api/hapus-akun.ts yang menerima POST dan bertindak berdasarkan cookie sesi, kamu baru saja membuat lubang CSRF. Fase 7 membahas cara menutupnya.

Latihan: buat action langganNewsletter lengkap dengan validasi Zod, lalu form yang memakainya. Uji empat jalur: (1) email valid → sukses; (2) email bukan-email → pesan error muncul di halaman; (3) matikan JavaScript di browser dan kirim lagi — form harus tetap bekerja; (4) kirim POST dari curl tanpa header Origin dan amati apa yang terjadi.

Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.