Panduan CORS dengan Claude Code: API cross-origin yang aman
Konfigurasi CORS dengan Claude Code: preflight, credentials, origin allowlist, perintah test, dan prompt review.
Mengonfigurasi CORS dengan benar memakai Claude Code
Frontend di localhost:3000 dan API di localhost:8787 sudah cukup untuk memunculkan error CORS di browser. Solusi cepat yang sering muncul adalah menambahkan Access-Control-Allow-Origin: *, tetapi konfigurasi ini berbahaya saat API memakai cookie, header Authorization, atau halaman admin.
CORS, Cross-Origin Resource Sharing, adalah mekanisme browser yang membuat server dapat menentukan origin mana yang boleh membaca responsnya. Origin adalah gabungan scheme, host, dan port. https://app.example.com, https://api.example.com, http://localhost:3000, dan http://localhost:5173 semuanya origin yang berbeda.
Panduan ini memecah konfigurasi CORS menjadi keputusan yang bisa direview setelah Claude Code menulis kode. Ada contoh siap pakai untuk Express, Fastify, Cloudflare Workers, dan Next.js Route Handler, plus preflight, credentials, origin allowlist, perintah test, dan prompt review Claude Code.
Poin paling penting: CORS bukan autentikasi. CORS hanya mengontrol apakah JavaScript di browser boleh membaca respons cross-origin. Ia tidak menghentikan curl, panggilan server-ke-server, atau pengguna tanpa izin. Autentikasi, otorisasi, CSRF, rate limiting, dan security headers tetap harus didesain terpisah.
sequenceDiagram
participant Browser as Browser
participant API as API server
Browser->>API: OPTIONS /api/messages<br/>Origin + Access-Control-Request-*
API-->>Browser: 204 + Access-Control-Allow-*
Browser->>API: POST /api/messages<br/>Cookie or Authorization
API-->>Browser: 200 + Access-Control-Allow-Origin
Keputusan sebelum menulis kode
Tentukan nilai berikut sebelum meminta Claude Code membuat middleware CORS. Requirement yang samar sering menghasilkan konfigurasi demo yang terlalu longgar untuk production.
| Keputusan | Contoh | Perhatikan |
|---|---|---|
| Origin yang diizinkan | https://app.example.com, https://admin.example.com | Tanpa path dan tanpa trailing slash |
| Credentials | Cookie, Authorization header | Untuk cookie, cek juga SameSite=None; Secure |
| Method | GET,POST,PUT,PATCH,DELETE,OPTIONS | Izinkan hanya yang dipakai API |
| Header | Content-Type,Authorization,X-Request-ID | Harus cocok dengan preflight |
Preflight adalah pengecekan izin dari browser sebelum request asli. Untuk POST JSON, Authorization, PUT, DELETE, dan banyak custom header, browser mengirim OPTIONS terlebih dahulu. Jika respons tidak memiliki Access-Control-Allow-Methods dan Access-Control-Allow-Headers yang sesuai, request asli tidak dikirim.
Konfigurasi Express
Contoh ini memakai Node.js 20 atau lebih baru. Middleware resmi cors untuk Express menerima fungsi pada origin, sehingga setiap request bisa dicek terhadap allowlist. Karena API mendukung credentials, hanya origin yang diizinkan yang direfleksikan dan credentials: true diaktifkan.
npm init -y
npm install express cors
node server.mjs
// server.mjs
import express from "express";
import cors from "cors";
const app = express();
const allowedOrigins = new Set([
"https://app.example.com",
"https://admin.example.com",
"http://localhost:3000",
"http://localhost:5173",
]);
function isAllowedOrigin(origin) {
if (!origin) return true;
if (allowedOrigins.has(origin)) return true;
return process.env.NODE_ENV !== "production" && /^http:\/\/localhost:\d+$/.test(origin);
}
const corsOptions = {
origin(origin, callback) {
callback(null, isAllowedOrigin(origin));
},
credentials: true,
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
allowedHeaders: ["Content-Type", "Authorization", "X-Request-ID"],
exposedHeaders: ["X-Request-ID"],
maxAge: 86400,
optionsSuccessStatus: 204,
};
app.use((req, res, next) => {
const origin = req.headers.origin;
if (origin && !isAllowedOrigin(origin)) {
return res.status(403).json({ error: "Origin not allowed" });
}
next();
});
app.use(cors(corsOptions));
app.use(express.json());
app.get("/api/health", (_req, res) => {
res.setHeader("X-Request-ID", crypto.randomUUID());
res.json({ ok: true });
});
app.post("/api/messages", (req, res) => {
res.setHeader("X-Request-ID", crypto.randomUUID());
res.json({ ok: true, received: req.body });
});
app.listen(8787, () => {
console.log("API listening on http://localhost:8787");
});
Di production, jalankan dengan NODE_ENV=production dan sisakan hanya domain nyata di allowedOrigins. Request tanpa header Origin bukan request CORS browser, sehingga contoh ini mengizinkannya; API key, JWT, dan izin pengguna tetap perlu dicek di middleware autentikasi.
Konfigurasi Fastify
Fastify memakai @fastify/cors. README resminya mendukung boolean, string, array, RegExp, dan function untuk origin, tetapi exact match memakai Set lebih mudah diaudit. Hindari regex yang terlalu luas kecuali ada alasan kuat.
npm init -y
npm install fastify @fastify/cors
node server.mjs
// server.mjs
import Fastify from "fastify";
import cors from "@fastify/cors";
const app = Fastify({ logger: true });
const allowedOrigins = new Set([
"https://app.example.com",
"https://admin.example.com",
"http://localhost:3000",
"http://localhost:5173",
]);
function isAllowedOrigin(origin) {
if (!origin) return true;
if (allowedOrigins.has(origin)) return true;
return process.env.NODE_ENV !== "production" && /^http:\/\/localhost:\d+$/.test(origin);
}
app.addHook("onRequest", async (request, reply) => {
const origin = request.headers.origin;
if (origin && !isAllowedOrigin(origin)) {
return reply.code(403).send({ error: "Origin not allowed" });
}
});
await app.register(cors, {
origin(origin, callback) {
callback(null, isAllowedOrigin(origin));
},
credentials: true,
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
allowedHeaders: ["Content-Type", "Authorization", "X-Request-ID"],
exposedHeaders: ["X-Request-ID"],
maxAge: 86400,
strictPreflight: true,
});
app.get("/api/health", async () => ({ ok: true }));
app.post("/api/messages", async (request) => {
return { ok: true, received: request.body };
});
await app.listen({ port: 8787, host: "0.0.0.0" });
Di Fastify, urutan plugin dan hook penting. Jika hook autentikasi menolak OPTIONS sebelum plugin CORS menjawab, browser tidak akan mengirim request asli. Minta Claude Code memeriksa urutan registrasi, bukan hanya nama header.
Konfigurasi Cloudflare Workers
Cloudflare Workers memakai Fetch API standar. Tangani OPTIONS secara eksplisit, tambahkan header CORS pada respons sukses dan error, lalu gunakan Vary: Origin saat respons berubah berdasarkan origin.
// src/index.ts
const allowedOrigins = new Set([
"https://app.example.com",
"https://admin.example.com",
"http://localhost:3000",
]);
function getCorsHeaders(request: Request): HeadersInit | null {
const origin = request.headers.get("Origin");
if (!origin) return {};
if (!allowedOrigins.has(origin)) return null;
return {
"Access-Control-Allow-Origin": origin,
"Access-Control-Allow-Credentials": "true",
"Access-Control-Allow-Methods": "GET,POST,OPTIONS",
"Access-Control-Allow-Headers": "Content-Type,Authorization,X-Request-ID",
"Access-Control-Max-Age": "86400",
"Vary": "Origin",
};
}
export default {
async fetch(request: Request): Promise<Response> {
const corsHeaders = getCorsHeaders(request);
if (corsHeaders === null) {
return Response.json({ error: "Origin not allowed" }, { status: 403 });
}
if (request.method === "OPTIONS") {
return new Response(null, { status: 204, headers: corsHeaders });
}
const url = new URL(request.url);
if (url.pathname === "/api/messages" && request.method === "POST") {
const body = await request.json().catch(() => ({}));
return Response.json({ ok: true, received: body }, { headers: corsHeaders });
}
return Response.json({ error: "Not found" }, { status: 404, headers: corsHeaders });
},
};
Kesalahan umum di Workers adalah memasang header hanya pada jalur sukses. Jika OPTIONS, 401, 403, atau 500 tidak memiliki CORS, DevTools bisa hanya menampilkan kegagalan CORS dan menyembunyikan error aplikasi yang sebenarnya.
Konfigurasi Next.js Route Handler
Dengan App Router, app/api/.../route.ts memakai Request dan Response standar Web. Dokumentasi Next.js menunjukkan cara menambahkan header CORS ke respons; untuk API dengan credentials, gunakan allowlist, bukan *.
// app/api/messages/route.ts
const allowedOrigins = new Set([
"https://app.example.com",
"https://admin.example.com",
"http://localhost:3000",
]);
function getCorsHeaders(request: Request): HeadersInit | null {
const origin = request.headers.get("Origin");
if (!origin) return {};
if (!allowedOrigins.has(origin)) return null;
return {
"Access-Control-Allow-Origin": origin,
"Access-Control-Allow-Credentials": "true",
"Access-Control-Allow-Methods": "POST,OPTIONS",
"Access-Control-Allow-Headers": "Content-Type,Authorization,X-Request-ID",
"Access-Control-Max-Age": "86400",
"Vary": "Origin",
};
}
export async function OPTIONS(request: Request) {
const headers = getCorsHeaders(request);
if (headers === null) {
return Response.json({ error: "Origin not allowed" }, { status: 403 });
}
return new Response(null, { status: 204, headers });
}
export async function POST(request: Request) {
const headers = getCorsHeaders(request);
if (headers === null) {
return Response.json({ error: "Origin not allowed" }, { status: 403 });
}
const body = await request.json().catch(() => ({}));
return Response.json({ ok: true, received: body }, { headers });
}
headers() di next.config.js cocok untuk header statis pada API publik. Jika origin harus dievaluasi per request, logika di Route Handler lebih mudah direview.
Perintah test
Gunakan curl untuk memisahkan preflight dan request asli. Pastikan Access-Control-Allow-Origin sama persis dengan Origin yang dikirim, dan Access-Control-Allow-Credentials: true hanya muncul untuk origin yang diizinkan.
curl -i -X OPTIONS http://localhost:8787/api/messages \
-H "Origin: http://localhost:3000" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type, Authorization"
curl -i -X POST http://localhost:8787/api/messages \
-H "Origin: http://localhost:3000" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer dev-token" \
--data '{"text":"hello"}'
curl -i -X OPTIONS http://localhost:8787/api/messages \
-H "Origin: https://evil.example" \
-H "Access-Control-Request-Method: POST"
Di browser, test credentials bisa seperti ini. Jika credentials: "include" dipakai, respons CORS wildcard akan ditolak browser.
await fetch("http://localhost:8787/api/messages", {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer dev-token",
},
body: JSON.stringify({ text: "hello" }),
});
Use case nyata
Use case pertama adalah SPA dan API di domain berbeda. Jika React berada di https://app.example.com dan API di https://api.example.com, diperlukan allowlist eksplisit. Jika memakai login cookie, review credentials, atribut cookie, dan CSRF sekaligus.
Use case kedua adalah frontend admin. Anda bisa menambahkan https://admin.example.com ke allowlist, tetapi CORS bukan pengganti pengecekan izin admin. Otorisasi tetap harus berada di kode API.
Use case ketiga adalah Cloudflare Worker sebagai BFF atau proxy ringan. Browser memanggil Worker, lalu Worker memanggil upstream API. Respons Worker ke browser tetap membutuhkan header CORS yang benar.
Use case keempat adalah API publik read-only. Jika tidak ada cookie, Authorization, atau data privat, Access-Control-Allow-Origin: * bisa diterima. Jika autentikasi mungkin ditambahkan nanti, mulai dengan allowlist sejak awal.
Jebakan spesifik
| Jebakan | Dampak | Perbaikan |
|---|---|---|
Menggabungkan * dengan credentials: true | Browser memblokir respons | Kembalikan origin eksplisit |
Menyimpan https://app.example.com/ | Trailing slash membuat mismatch | Simpan https://app.example.com |
Hanya mengizinkan localhost | Port berbeda gagal | Tulis http://localhost:3000 |
Mewajibkan auth untuk OPTIONS | Preflight berhenti di 401/403 | Tangani preflight sebelum auth |
| CORS hilang pada error | DevTools menyembunyikan error asli | Tambahkan header pada 4xx/5xx |
| CDN cache header per origin | Header bercampur antar origin | Tambahkan Vary: Origin |
| Menganggap CORS sebagai otorisasi | Client non-browser tetap bisa memanggil API | Implementasikan auth dan CSRF terpisah |
MDN jelas menyatakan request CORS dengan credentials tidak bisa memakai Access-Control-Allow-Origin: *. Jika Claude Code menghasilkan kombinasi itu, anggap sebagai bug yang harus diperbaiki.
Prompt review untuk Claude Code
Review this repository's CORS configuration.
Check:
- No Access-Control-Allow-Origin: * when credentials are enabled
- Allowlist uses exact scheme/host/port matching
- OPTIONS preflight runs before authentication middleware
- 4xx/5xx responses include the required CORS headers
- Vary: Origin is present when responses vary by origin
If changes are needed, propose the smallest safe diff.
Diagnose this CORS error by cause.
Browser error:
<paste the full DevTools Console message>
curl preflight:
<paste curl -i -X OPTIONS output>
Expected origin:
https://app.example.com
Read the relevant API files and return reproduction steps, root cause, fix, and tests.
Review the Express/Fastify/Next.js/Workers CORS implementation as a security reviewer.
Focus on:
- Whether request origins are blindly reflected
- Whether localhost remains enabled in production
- Whether Authorization is allowed without proper authorization checks
- Whether cookie flows mention SameSite=None; Secure and CSRF protection
- Whether test commands separate preflight and the real request
Group findings as Critical, Must fix, and Improvement.
Referensi resmi dan link internal
Gunakan panduan CORS MDN sebagai dasar. Untuk implementasi, lihat Express cors middleware, @fastify/cors, contoh CORS Cloudflare Workers, dan Next.js Route Handlers. Untuk workflow Claude Code yang dapat dipakai ulang, lihat Claude Code commands.
Baca juga panduan pengembangan API, panduan web security headers, panduan Cloudflare Workers, dan checklist workflow code review.
Langkah berikutnya
Setelah mengganti domain contoh dengan domain Anda, jalankan prompt review pada repository lalu gunakan Claude Code security best practices untuk mengecek cookie, CSRF, otorisasi, dan headers sekaligus. Untuk proyek klien atau platform internal, checklist ini bisa menjadi artefak review yang mendukung penawaran bantuan implementasi berbayar atau adopsi template reusable.
Hasil setelah benar-benar dicoba
Dalam test lokal Masa, contoh Express dan Fastify berjalan di localhost:8787; preflight dan POST dari Origin: http://localhost:3000 berhasil, sedangkan https://evil.example mengembalikan 403. Bagian yang paling mudah terlewat adalah header CORS pada respons error dan penanganan OPTIONS eksplisit di Workers. Alur paling stabil adalah membuat allowlist lebih dulu, menjalankan curl, lalu meminta Claude Code memastikan tidak ada kombinasi wildcard plus credentials, Vary: Origin ada di tempat yang perlu, dan localhost tidak ikut masuk production.
PDF gratis: cheatsheet Claude Code
Masukkan email dan unduh satu halaman berisi command, kebiasaan review, dan workflow aman.
Kami menjaga datamu dan tidak mengirim spam.
Tentang penulis
Masa
Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.
Artikel terkait
Workflow Obsidian ke CLAUDE.md untuk Claude Code
Ubah catatan kerja Obsidian menjadi operating note CLAUDE.md agar konteks tidak dijelaskan ulang.
Claude Code Revenue CTA Routing: dari artikel ke PDF, Gumroad, dan konsultasi
Workflow Claude Code untuk mengarahkan pembaca ke PDF gratis, Gumroad, atau konsultasi sesuai intent.
Aturan handoff tim Claude Code: bukti review, permission, rollback, dan jalur revenue
Format handoff Claude Code untuk tim: bukti, permission rule, rollback, PDF gratis, Gumroad, dan konsultasi.