โ† Semua pembelajaran / Astro Nol โ†’ Portal Berita
Fase 3 ยท Jalur API

Menulis API di Astro

Astro bisa jadi penyaji halaman dan penyedia API sekaligus, dalam satu proses dan satu deploy. Untuk portal berukuran menengah, itu penyederhanaan yang berarti.

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

Intisari

  • Nama fungsi yang di-export adalah metode HTTP-nya. Metode yang tidak ada otomatis dijawab 405.
  • Kembalikan Response standar web โ€” sama seperti di Workers, Deno, dan Bun.
  • export const prerender = false wajib untuk endpoint dinamis, atau ia dibekukan saat build.
  • Endpoint tidak mendapat perlindungan CSRF otomatis seperti Actions. Kamu yang bertanggung jawab.
  • Sitemap dan RSS adalah kasus terbaiknya: dihasilkan dari database, di-cache lama di edge.

Bentuk dasarnya

// src/pages/api/artikel.json.ts  โ†’  /api/artikel.json
import type { APIRoute } from "astro";
import { ambilArtikelTerbaru } from "@/lib/artikel";

export const prerender = false;

export const GET: APIRoute = async ({ url }) => {
  const kategori = url.searchParams.get("kategori") ?? "semua";
  const batas = Math.min(Number(url.searchParams.get("batas") ?? 20), 100);

  const artikel = await ambilArtikelTerbaru(kategori, batas);

  return new Response(JSON.stringify({ data: artikel }), {
    status: 200,
    headers: {
      "Content-Type": "application/json; charset=utf-8",
      "Cache-Control": "public, max-age=0, s-maxage=120, stale-while-revalidate=600",
    },
  });
};

Math.min(โ€ฆ, 100) itu bukan detail. Tanpa batas atas, siapa pun bisa memanggil ?batas=1000000 dan memaksa database memindai seluruh tabel lalu menyusun JSON ratusan megabita. Satu request seperti itu bisa menghabiskan memori container-mu. Setiap parameter numerik dari luar butuh batas atas โ€” selalu.

Metode HTTP

export const GET: APIRoute = async (ctx) => { โ€ฆ };
export const POST: APIRoute = async (ctx) => { โ€ฆ };
export const DELETE: APIRoute = async (ctx) => { โ€ฆ };

// Semua metode lain otomatis dijawab 405 Method Not Allowed.
// Tidak perlu menulis pemeriksaan apa pun.

Bandingkan dengan CI3: satu method controller menerima GET dan POST tanpa membedakan, dan $this->input->method() mudah terlupa. Endpoint yang menerima POST untuk menghapus sesuatu tapi juga bisa dipicu lewat GET adalah lubang keamanan klasik โ€” dan di sini tidak mungkin terjadi.

Rute dinamis untuk endpoint

// src/pages/api/artikel/[slug].json.ts
export const GET: APIRoute = async ({ params }) => {
  const artikel = await ambilArtikelPublik(params.slug!);

  if (!artikel) {
    return new Response(
      JSON.stringify({ error: "Artikel tidak ditemukan" }),
      { status: 404, headers: { "Content-Type": "application/json" } },
    );
  }

  // Jangan pernah mengirim isi premium lewat API publik.
  const { isi, ...aman } = artikel;

  return new Response(
    JSON.stringify({ data: artikel.premium ? aman : artikel }),
    {
      status: 200,
      headers: {
        "Content-Type": "application/json",
        "Cache-Control": "public, s-maxage=300",
        "Cache-Tag": `artikel-${artikel.id}`,
      },
    },
  );
};

Baris const { isi, ...aman } itu pola yang layak dibiasakan. API paywall yang bocor hampir selalu bocor lewat endpoint JSON, bukan lewat halaman โ€” karena orang mengingat memasang paywall di HTML lalu lupa bahwa API yang sama menyajikan artikel yang sama. Bangun kebiasaan menyusun bentuk keluaran secara eksplisit, bukan meneruskan baris database apa adanya.

Kasus terbaik: sitemap dan RSS dari database

// src/pages/sitemap-berita.xml.ts
import type { APIRoute } from "astro";
import { ambilArtikelUntukSitemap } from "@/lib/artikel";

export const prerender = false;

export const GET: APIRoute = async ({ site }) => {
  // Google News hanya membaca artikel 2 hari terakhir.
  const artikel = await ambilArtikelUntukSitemap(2);

  const xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
        xmlns:news="http://www.google.com/schemas/sitemap-news/0.9">
${artikel
  .map(
    (a) => `  <url>
    <loc>${new URL(`/berita/${a.slug}`, site).href}</loc>
    <news:news>
      <news:publication>
        <news:name>Portal Contoh</news:name>
        <news:language>id</news:language>
      </news:publication>
      <news:publication_date>${a.terbitPada.toISOString()}</news:publication_date>
      <news:title>${escXml(a.judul)}</news:title>
    </news:news>
  </url>`,
  )
  .join("\n")}
</urlset>`;

  return new Response(xml, {
    headers: {
      "Content-Type": "application/xml; charset=utf-8",
      "Cache-Control": "public, s-maxage=300",
    },
  });
};

function escXml(s: string) {
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;")
          .replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}

escXml itu wajib. Judul berita mengandung & lebih sering daripada yang kamu kira โ€” "Bank A & Bank B merger" cukup untuk membuat seluruh sitemap-mu tidak valid, dan Google akan berhenti membacanya tanpa memberitahumu.

Yang TIDAK kamu dapat dari endpoint

FiturActionsEndpoint .ts
Validasi input otomatisYa (Zod)Tidak โ€” tulis sendiri
Perlindungan CSRFYa (cek Origin)Tidak
Tipe otomatis di klienYaTidak
Kendali penuh status & headerTerbatasYa
Akses raw bodyTidakYa
// Endpoint yang bertindak berdasarkan sesi WAJIB memeriksa Origin sendiri.
function asalSah(request: Request, site: URL) {
  const origin = request.headers.get("origin");
  return origin !== null && origin === site.origin;
}

export const POST: APIRoute = async ({ request, site, cookies }) => {
  if (!asalSah(request, site!)) {
    return new Response(null, { status: 403 });
  }
  // โ€ฆ
};

Pengecualiannya adalah webhook. Midtrans tidak mengirim header Origin milikmu, dan memang tidak seharusnya. Webhook diamankan dengan verifikasi tanda tangan, bukan CSRF โ€” dan karena itu ia harus berada di path yang tidak ikut aturan CSRF-mu. Fase 6.

Header yang sering terlupa

HeaderKapan
Content-Type: application/json; charset=utf-8Selalu. Tanpa charset, judul berbahasa Indonesia dengan karakter khusus bisa rusak di sebagian klien
Cache-ControlSelalu, eksplisit. Diam berarti menyerahkan keputusan ke default Cloudflare
Access-Control-Allow-OriginHanya kalau memang dipanggil dari domain lain โ€” jangan * untuk data yang butuh autentikasi
Vary: Accept-EncodingKalau responsnya bisa dikompres berbeda
X-Content-Type-Options: nosniffSelalu. Sudah dipasang middleware kalau kamu mengikuti Fase 1

Latihan: buat /api/artikel.json dengan parameter kategori dan batas yang dibatasi maksimum 100. Uji empat hal: ?batas=99999 harus dipotong; POST ke endpoint itu harus mengembalikan 405 tanpa kamu menulis pemeriksaan apa pun; responsnya punya Cache-Control yang eksplisit; dan artikel premium tidak mengirim kolom isi.

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