karawaci.kode

← Semua snippet

TypeScript Menengah Performance

Edge runtime fetch dengan cache header

Tune cache header di edge runtime (Vercel, Cloudflare Workers) — s-maxage, stale-while-revalidate, dan tag-based purge. Latency 30ms global.

Dipublikasikan 14 Juni 2026

API endpoint daftar produk Tokopedia di-call ribuan kali per detik. Tanpa cache, server meledak. Pakai edge runtime + cache header yang tepat, hit rate bisa 95% — origin cuma di-hit untuk revalidation. Snippet ini route handler Next.js edge dengan caching strategi lengkap.

Kode

// app/api/produk/[id]/route.ts
import { NextResponse } from "next/server";

// Pin ke edge runtime — eksekusi di POP terdekat user
export const runtime = "edge";

interface Produk {
  id: number;
  nama: string;
  harga: number;
  stok: number;
  updated_at: string;
}

async function fetchProdukDariOrigin(id: string): Promise<Produk> {
  const r = await fetch(`${process.env.ORIGIN_API_URL}/produk/${id}`, {
    headers: { "X-Internal-Key": process.env.INTERNAL_KEY! },
  });
  if (!r.ok) throw new Error(`Origin error: ${r.status}`);
  return r.json();
}

export async function GET(
  _request: Request,
  ctx: { params: Promise<{ id: string }> }
) {
  const { id } = await ctx.params;

  if (!/^\d+$/.test(id)) {
    return NextResponse.json(
      { error: "ID tidak valid" },
      { status: 400, headers: { "Cache-Control": "no-store" } }
    );
  }

  try {
    const produk = await fetchProdukDariOrigin(id);

    return NextResponse.json(produk, {
      headers: {
        // Browser cache 1 menit
        // CDN cache 5 menit, stale-while-revalidate 1 jam
        // Sehingga: hit dalam 5 menit instant
        //          hit 5-65 menit instant + background refresh
        //          hit > 65 menit re-fetch origin
        "Cache-Control":
          "public, max-age=60, s-maxage=300, stale-while-revalidate=3600",

        // Cache tag — bisa purge selective via API
        "Cache-Tag": `produk-${id},katalog`,

        // CDN bisa cache walaupun ada Set-Cookie (Vercel)
        "Vercel-CDN-Cache-Control": "max-age=300",
      },
    });
  } catch (err) {
    return NextResponse.json(
      { error: "Origin tidak tersedia" },
      {
        status: 503,
        headers: { "Cache-Control": "no-store" },
      }
    );
  }
}
// app/api/cache/purge/route.ts — endpoint internal untuk purge
import { NextResponse } from "next/server";

export const runtime = "edge";

export async function POST(req: Request) {
  // Auth: cek secret
  const auth = req.headers.get("X-Purge-Secret");
  if (auth !== process.env.PURGE_SECRET) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  const { tag } = (await req.json()) as { tag: string };

  // Vercel Cache API
  const purgeResp = await fetch(
    `https://api.vercel.com/v1/edge-config/${process.env.VERCEL_PROJECT_ID}/purge`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.VERCEL_API_TOKEN}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ tag }),
    }
  );

  if (!purgeResp.ok) {
    return NextResponse.json({ error: "Purge fail" }, { status: 502 });
  }

  return NextResponse.json({ ok: true, tag });
}

Pemakaian

// Client fetch — biarkan browser handle cache via header
const produk = await fetch("/api/produk/123").then((r) => r.json());

// Saat admin update produk, trigger purge
async function updateProduk(id: number, data: Partial<Produk>) {
  await fetch(`/api/admin/produk/${id}`, {
    method: "PUT",
    body: JSON.stringify(data),
  });

  // Invalidate cache CDN
  await fetch("/api/cache/purge", {
    method: "POST",
    headers: { "X-Purge-Secret": INTERNAL_SECRET },
    body: JSON.stringify({ tag: `produk-${id}` }),
  });
}
// Bulk purge — saat ada katalog campaign update
await fetch("/api/cache/purge", {
  method: "POST",
  headers: { "X-Purge-Secret": INTERNAL_SECRET },
  body: JSON.stringify({ tag: "katalog" }),
});
// Semua produk dengan tag "katalog" di-evict

Kapan dipakai

  • API publik dengan response yang gak sering berubah (katalog produk, list kategori).
  • SEO landing page yang content stabil per hari/jam.
  • Image proxy / transformation endpoint.
  • Public API rate-limited yang banyak diakses dari client.

Catatan

  • s-maxage vs max-age — pakai keduanya. s-maxage tinggi (CDN lama), max-age rendah (browser refresh sering biar dapat update setelah purge).
  • stale-while-revalidate — magic untuk perceived perf. User dapat data lama 1 detik, sementara CDN refresh di background.
  • Cache-Tag — Vercel & Cloudflare support tag-based purge. Lebih clean dari URL-based.
  • no-store untuk error — jangan cache 4xx/5xx response. Kalau di-cache, error stuck sampai TTL habis.
  • Vary header — kalau response tergantung Accept-Language, Authorization, dll, set Vary: Authorization, Accept-Language. Tanpa ini, CDN serve response salah ke user beda.

Cache hit rate tinggi cuma penting kalau origin lambat. Untuk endpoint yang sudah cepat (<50ms), overhead CDN bisa lebih besar dari hit langsung. Profile dulu sebelum bertumpuk cache layer.

# tags

edge-runtimecacheswrcdnperformance

Ditulis oleh Asti Larasati · 14 Juni 2026