Langsung ke konten
KamusNgoding
Menengah Laravel 4 menit baca

Cara Mengatasi Error CORS di Laravel + React/Vue

#laravel #cors #api #react #vue #axios #error #intermediate

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! 🎉

Artikel Terkait