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 (
ActionErrorcode + 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
-
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. -
Server Functions CSRF: harus implement manual. Saya pakai pattern double-submit cookie. Detail di post CSRF di SPA 2026.
-
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.
-
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