karawaci.kode

2026-06-07 · 6 min

Astro 6 Actions vs Server Functions: Production Pakai Mana

Astro 6 punya dua cara untuk handle server-side logic dari client: Actions (type-safe RPC) dan Server Functions (endpoint API klasik). Saya pakai keduanya di production selama 4 bulan, tiap-tiap di project klien beda. Comparison real.

Setup project

Project A (Actions): SaaS dashboard internal klien Jakarta, ~200 user internal, banyak form (CRUD karyawan, payroll, leave request). 18 form total.

Project B (Actions): Landing page warung kuliner Tangerang dengan booking form, reservation, feedback. Lihat detail di migrasi design system shadcn + Tailwind v4 yang related.

Project C (Server Functions): Public API untuk integrasi POS warung dengan accounting software klien. ~40 endpoint REST.

Actions: DX di form-heavy

Astro Actions pakai pattern type-safe:

// src/actions/index.ts
import { defineAction } from 'astro:actions';
import { z } from 'astro:schema';

export const server = {
  createEmployee: defineAction({
    accept: 'form',
    input: z.object({
      name: z.string().min(2),
      email: z.string().email(),
      salary: z.number().positive(),
    }),
    handler: async (input, context) => {
      const user = context.locals.user;
      if (!user) throw new ActionError({ code: 'UNAUTHORIZED' });
      
      return await db.insert(employees).values({
        ...input,
        tenantId: user.tenantId,
      });
    },
  }),
};

Client call:

<form method="POST" action={actions.createEmployee}>
  <input name="name" />
  ...
</form>

Yang saya suka:

  • Zod schema sekali, type otomatis ke client
  • Form action native, progressive enhancement gratis (works tanpa JS)
  • Error handling structured (ActionError code + message)
  • accept: 'form' parse FormData otomatis

Form processing di Project A: 18 form, total code (action + form binding) ~1,200 LOC. Reference baseline (kalau pakai REST manual + fetch + validation client + server): estimate ~2,400 LOC. Reduction ~50%.

Server Functions: API REST-like

Astro Server Functions = endpoint API klasik di src/pages/api/*.ts:

// src/pages/api/transactions.ts
export const POST: APIRoute = async ({ request }) => {
  const body = await request.json();
  const parsed = transactionSchema.safeParse(body);
  if (!parsed.success) {
    return new Response(JSON.stringify({ error: parsed.error }), {
      status: 400,
    });
  }
  
  // ... process
  return Response.json({ id: '...', status: 'created' });
};

Project C: 40 endpoint, full REST semantics (GET, POST, PUT, DELETE), versi v1 dan v2 (klien butuh backward compat).

Yang Server Functions cocok:

  • Public-facing API yang dipanggil mobile client / partner integration
  • Webhook receiver (gateway payment, WhatsApp Business)
  • File upload dengan multipart parsing custom
  • Streaming response (Server-Sent Events untuk notification)

Saya pakai pattern dengan zod-validation helper utility yang shared, akhirnya tetap clean.

Latency

Project A (Actions, internal app, low traffic):

  • P50: 180ms (form submit → render result)
  • P95: 380ms

Project C (Server Functions, integration partner, ~500 req/min):

  • P50: 95ms
  • P95: 220ms

Server Functions sedikit lebih cepat di latency murni karena tidak ada form action redirect overhead. Tapi untuk Actions, perceived latency lebih bagus karena progressive enhancement (form submit feel native).

Memory & cost

Sama-sama di Cloudflare Pages (project A, B) dan Hetzner Bun runtime (project C). Tidak ada perbedaan memory atau cost yang signifikan dari pilihan Actions vs Server Functions.

Yang break

  1. Actions tanpa JS: progressive enhancement claim true 80% case. Tapi useActionState (client-side feedback) butuh JS. User dengan ad-blocker yang aggressive (kasus di Project A: 3 user) reporting form behavior weird.

  2. Server Functions CSRF: harus implement manual. Saya pakai pattern double-submit cookie. Detail di post CSRF di SPA 2026.

  3. File upload di Actions: support FormData dengan File, tapi limit ~10MB sebelum mulai trouble (memory spike di edge runtime). Untuk upload besar, switch ke pre-signed URL ke R2.

  4. Migrasi schema input: Actions pakai astro:schema (re-export Zod), Server Functions bisa pakai Zod direct. Konsisten kalau pilih satu, tapi mixing oke juga.

Pola yang saya adopt

Hybrid pattern di project mixed (kasus saya: ada satu project klien yang punya internal dashboard + public API):

  • Internal dashboard: pakai Actions. Type-safe, less boilerplate.
  • Public API + webhook + mobile-facing: pakai Server Functions. Full HTTP semantics, versionable.

Akses ke shared business logic via src/lib/*.ts. Action dan API endpoint sama-sama call ke lib.

src/
├── actions/        # Internal RPC
├── pages/api/      # Public API
└── lib/            # Shared business logic

Kapan saya hindari Actions

  • App yang punya mobile native client share business logic (Flutter, React Native). Actions susah dipanggil dari non-web client.
  • API yang punya partner external integration (mereka expect REST, bukan RPC).
  • Endpoint yang butuh streaming atau file upload besar.

Verdict

Untuk Astro app dengan dashboard / form internal: Actions clear winner. Untuk Astro yang sekaligus public API: Server Functions atau hybrid.

Bukan pilihan exclusive — pakai keduanya sesuai use case. Sama pattern dengan Server Actions di Next.js yang saya tulis kemarin: tooling baru menggantikan boilerplate, tapi tetap butuh pemikiran arsitektur.

Pattern saya yang saya pasang sekarang di semua project Astro 6: default Actions untuk form, Server Functions untuk API. Sederhana, konsisten, bisa dijelaskan ke junior tim dalam 15 menit.

Ditulis oleh Reza Pradipta