Pendahuluan
Kamu baru saja selesai membangun API Laravel yang bekerja sempurna di Postman. Semua endpoint merespons dengan benar, data JSON keluar rapi. Lalu kamu coba panggil dari aplikasi React atau Vue… dan tiba-tiba ini muncul di browser console:
Access to XMLHttpRequest at 'http://localhost:8000/api/tasks'
from origin 'http://localhost:5173' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
Selamat datang di CORS Hell — masalah yang paling sering ditanyakan developer Indonesia di Stack Overflow dan forum lokal.
Kabar baiknya: setelah membaca artikel ini, kamu akan sepenuhnya memahami mengapa error ini terjadi dan bagaimana mengatasinya dengan benar. Bukan hanya copy-paste solusi yang tidak dimengerti.
Apa itu CORS?
CORS (Cross-Origin Resource Sharing) adalah mekanisme keamanan browser yang mengontrol apakah sebuah halaman web dapat meminta resource dari domain yang berbeda.
Same-Origin Policy
Browser memiliki aturan dasar: halaman web hanya boleh meminta resource dari “origin” yang sama. Origin didefinisikan sebagai kombinasi dari:
origin = protokol + domain + port
http://localhost:3000 ≠ http://localhost:8000 (port berbeda)
https://example.com ≠ http://example.com (protokol berbeda)
https://api.example.com ≠ https://example.com (subdomain berbeda)
Mengapa CORS Muncul di Laravel + React/Vue?
Dulu, developer Indonesia membangun aplikasi dengan Monolithic Architecture — Laravel Blade yang render HTML langsung. Frontend dan backend satu domain, tidak ada CORS.
Tren modern adalah Decoupled Architecture:
Frontend (React/Vue) Backend (Laravel API)
http://localhost:5173 http://localhost:8000
│ │
└── fetch('/api/tasks') ─┤
│
Browser: "STOP! Origin berbeda!"
→ CORS Error
Cara Kerja CORS
Ada dua jenis CORS request:
1. Simple Request
Request “sederhana” (GET/POST dengan header standar) langsung dikirim. Server harus merespons dengan header Access-Control-Allow-Origin.
Browser → GET /api/tasks (dengan header Origin: http://localhost:5173)
Server → Respons + "Access-Control-Allow-Origin: http://localhost:5173"
Browser → ✅ OK, tampilkan data
2. Preflight Request (OPTIONS)
Untuk request yang “tidak sederhana” (DELETE, PUT, custom headers, JSON body), browser terlebih dahulu mengirim request OPTIONS untuk “bertanya izin”:
Browser → OPTIONS /api/tasks
Headers:
Origin: http://localhost:5173
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: Content-Type, Authorization
Server → 200 OK
Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Browser → DELETE /api/tasks/42 ← Baru kirim request sebenarnya
Solusi #1: Konfigurasi config/cors.php (Cara Resmi)
Laravel sudah menyertakan middleware CORS bawaan. Kamu hanya perlu mengkonfigurasinya:
<?php
// config/cors.php
return [
// Route mana yang terkena CORS rule ini
'paths' => ['api/*', 'sanctum/csrf-cookie'],
// Allowed methods — gunakan '*' untuk semua, atau spesifikkan
'allowed_methods' => ['*'],
// Allowed origins — JANGAN gunakan '*' di production!
'allowed_origins' => [
'http://localhost:5173', // Vite dev server (React/Vue)
'http://localhost:3000', // Create React App dev server
'https://myapp.com', // Domain production frontend
],
// Pattern untuk allowed origins (regex)
'allowed_origins_patterns' => [],
// Headers yang boleh dikirim oleh client
'allowed_headers' => ['*'],
// Headers yang boleh dibaca oleh client dari respons
'exposed_headers' => [],
// Berapa lama (detik) browser cache hasil preflight
'max_age' => 0,
// Apakah cookies/credentials boleh dikirim?
'supports_credentials' => false, // Ubah ke true jika butuh cookies/session
];
Pastikan Middleware CORS Aktif
Di Laravel 11+, middleware CORS sudah otomatis aktif untuk route API. Verifikasi di bootstrap/app.php:
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
$middleware->api(prepend: [
\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
]);
})
Jika menggunakan Laravel 10 ke bawah, pastikan HandleCors ada di app/Http/Kernel.php:
// app/Http/Kernel.php
protected $middleware = [
\Illuminate\Http\Middleware\HandleCors::class, // Pastikan ada
// ...
];
Solusi #2: Penggunaan dengan Axios (Frontend React/Vue)
// Di React/Vue — menggunakan axios
import axios from 'axios';
// Konfigurasi base URL sekali
const api = axios.create({
baseURL: 'http://localhost:8000/api',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
});
// Panggil API
const fetchTasks = async () => {
try {
const response = await api.get('/tasks');
return response.data;
} catch (error) {
console.error('Error:', error.response?.data);
}
};
// Jika butuh credentials (session/cookies dengan Sanctum SPA)
const api = axios.create({
baseURL: 'http://localhost:8000',
withCredentials: true, // Wajib untuk Sanctum SPA auth
});
// Dan di config/cors.php, ubah:
// 'supports_credentials' => true,
// 'allowed_origins' => ['http://localhost:5173'], // JANGAN gunakan '*' jika credentials=true
Solusi #3: Konfigurasi Nginx untuk Production
Di production, salah satu solusi terbaik adalah menggunakan Nginx sebagai reverse proxy — frontend dan backend dijalankan dari domain yang sama, sehingga CORS tidak terjadi sama sekali.
# /etc/nginx/sites-available/myapp.conf
server {
listen 80;
server_name myapp.com;
# Frontend React/Vue (build artifact)
location / {
root /var/www/frontend/dist;
try_files $uri $uri/ /index.html;
}
# Backend Laravel API — semua /api/* diteruskan ke Laravel
location /api/ {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Dengan konfigurasi ini, browser hanya melihat satu domain (myapp.com), tidak ada CORS sama sekali. Ini adalah cara paling bersih di production.
Contoh Lengkap: Laravel API + React
Backend (Laravel)
// routes/api.php
use App\Http\Controllers\Api\TaskController;
Route::apiResource('tasks', TaskController::class);
// app/Http/Controllers/Api/TaskController.php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Task;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
class TaskController extends Controller
{
public function index(): JsonResponse
{
$tasks = Task::latest()->paginate(15);
return response()->json($tasks);
}
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'priority' => 'required|in:low,medium,high',
]);
$task = Task::create($validated);
return response()->json($task, 201);
}
public function update(Request $request, Task $task): JsonResponse
{
$task->update($request->validated());
return response()->json($task);
}
public function destroy(Task $task): JsonResponse
{
$task->delete();
return response()->json(null, 204);
}
}
Frontend (React)
// src/App.jsx
import { useState, useEffect } from 'react';
import axios from 'axios';
const api = axios.create({
baseURL: 'http://localhost:8000/api',
headers: { 'Accept': 'application/json' },
});
function App() {
const [tasks, setTasks] = useState([]);
const [newTask, setNewTask] = useState('');
useEffect(() => {
api.get('/tasks').then(res => setTasks(res.data.data));
}, []);
const addTask = async (e) => {
e.preventDefault();
const res = await api.post('/tasks', { title: newTask, priority: 'medium' });
setTasks(prev => [res.data, ...prev]);
setNewTask('');
};
const deleteTask = async (id) => {
await api.delete(`/tasks/${id}`);
setTasks(prev => prev.filter(t => t.id !== id));
};
return (
<div>
<h1>Task Manager</h1>
<form onSubmit={addTask}>
<input value={newTask} onChange={e => setNewTask(e.target.value)}
placeholder="Tambah task baru..." />
<button type="submit">Tambah</button>
</form>
{tasks.map(task => (
<div key={task.id}>
<span>{task.title}</span>
<button onClick={() => deleteTask(task.id)}>Hapus</button>
</div>
))}
</div>
);
}
Baca lebih lanjut tentang komponen React di panduan React di KamusNgoding.
Troubleshooting: Error yang Sering Muncul
Response to preflight request doesn't pass access control check
Penyebab: Server tidak merespons request OPTIONS dengan benar. Sering terjadi karena Nginx/Apache di production tidak meneruskan OPTIONS request ke Laravel.
Solusi untuk Nginx:
location /api/ {
# Handle OPTIONS preflight
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' 'https://myapp.com';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, Accept';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Length' 0;
return 204;
}
proxy_pass http://127.0.0.1:8000;
}
Cookies/Session Tidak Terkirim Meski withCredentials: true
Penyebab: supports_credentials di config/cors.php masih false, atau allowed_origins menggunakan wildcard *.
Solusi:
// config/cors.php
'allowed_origins' => ['http://localhost:5173'], // JANGAN pakai '*'
'supports_credentials' => true,
// Di frontend
axios.defaults.withCredentials = true;
Header CORS Muncul Dua Kali (Duplicate Headers)
Penyebab: CORS header ditambahkan dua kali — sekali oleh Laravel, sekali oleh Nginx.
Solusi: Pilih salah satu: biarkan Laravel yang mengatur CORS (hapus konfigurasi CORS di Nginx), atau biarkan Nginx yang mengatur (disable middleware CORS di Laravel).
Pertanyaan yang Sering Diajukan
Kenapa di Postman tidak ada error CORS tapi di browser ada?
Karena CORS adalah kebijakan browser, bukan server. Postman adalah aplikasi desktop yang tidak mengikuti Same-Origin Policy. Server tidak pernah “menolak” request karena CORS — server selalu merespons. Browser-lah yang memeriksa header respons dan memutuskan apakah JavaScript boleh membaca hasilnya.
Apakah aman menggunakan 'allowed_origins' => ['*']?
Untuk development: tidak masalah. Untuk production: tidak aman jika menggunakan supports_credentials: true, karena membuka celah serangan CSRF. Selalu tentukan domain spesifik di production: ['https://myapp.com'].
Apa bedanya CORS mode di Sanctum vs config/cors.php?
config/cors.php mengatur CORS untuk semua route API secara umum. Laravel Sanctum memiliki mode khusus SPA Authentication yang menggunakan cookie session — mode ini memerlukan supports_credentials: true dan domain yang dikonfigurasi di SANCTUM_STATEFUL_DOMAINS di .env. Kita akan bahas lebih detail di artikel REST API dengan Sanctum.
Bagaimana cara debug CORS dengan benar?
# Gunakan curl untuk melihat header response secara lengkap
curl -I -X OPTIONS http://localhost:8000/api/tasks \
-H "Origin: http://localhost:5173" \
-H "Access-Control-Request-Method: GET"
# Pastikan ada header ini di response:
# Access-Control-Allow-Origin: http://localhost:5173
# Access-Control-Allow-Methods: GET, POST, ...
Kesimpulan
CORS bukan lagi misteri! Kita sudah memahami:
- Apa itu CORS dan mengapa Same-Origin Policy ada
- Cara kerja Preflight Request (OPTIONS)
- Solusi resmi via
config/cors.php - Konfigurasi Axios di frontend React/Vue
- Nginx Reverse Proxy untuk production tanpa CORS
- Penyebab umum dan cara debug yang tepat
Dengan pemahaman ini, kamu tidak hanya bisa mengatasi error CORS — kamu juga bisa menjelaskan ke anggota tim mengapa error itu terjadi dan cara mencegahnya.
Di artikel berikutnya: Authentication di Laravel dengan Laravel Breeze — sistem login, register, dan proteksi halaman yang bisa kamu tambahkan ke Task Manager dalam hitungan menit!
Tidak ada lagi CORS yang membuatmu pusing! 🎉