Programming Intermediate 9 min

How to Configure CORS on Your API the Right Way

CORS (Cross-Origin Resource Sharing) is enforced by the browser, not by the server. Your API does not "block" cross-origin requests — the browser does, based on headers your server provides. Understanding this distinction is the first step to configuring it correctly, and to knowing when a CORS error is actually a different problem in disguise.

Step-by-step

  1. 1

    Understand what CORS actually enforces

    When a browser makes a cross-origin request (different domain, port, or scheme), it checks the response for Access-Control-Allow-Origin. If the header is missing or does not match the requesting origin, the browser blocks the response from reaching JavaScript — the request still hit your server and your server still responded.

    This means CORS is not a security mechanism for your server — it is a browser policy that protects users from malicious websites making requests on their behalf. A server-to-server request (curl, Postman, another backend) is never affected by CORS.

  2. 2

    Never use a wildcard for credentialed requests

    Setting Access-Control-Allow-Origin: * allows any origin to read responses, but the browser will refuse to send cookies or Authorization headers to a wildcard origin. If your API relies on session cookies or bearer tokens, a wildcard cannot work — and you should not want it to, since it would allow any website to make authenticated calls on your users' behalf.

    bash
    // DO NOT use this for authenticated APIs
    Access-Control-Allow-Origin: *
    
    // DO: specify the exact origin
    Access-Control-Allow-Origin: https://app.example.com
  3. 3

    Allow-list a set of trusted origins

    For most APIs you have a small, known set of origins (your frontend, your admin panel, maybe a mobile web view). Validate the request's Origin header against that list and echo back only if it matches.

    javascript
    // Node.js / Express — cors middleware
    const cors = require('cors');
    
    const allowedOrigins = [
        'https://app.example.com',
        'https://admin.example.com',
    ];
    
    app.use(cors({
        origin: (origin, callback) => {
            // Allow server-to-server (no Origin header) or a known origin
            if (!origin || allowedOrigins.includes(origin)) {
                callback(null, true);
            } else {
                callback(new Error(`CORS: origin ${origin} not allowed`));
            }
        },
        credentials: true,
    }));
  4. 4

    Handle preflight OPTIONS requests

    For requests that use non-simple methods (PUT, DELETE, PATCH) or custom headers, the browser sends a preflight OPTIONS request first. Your server must respond with 200 or 204 and the appropriate CORS headers — otherwise the actual request is never sent.

    bash
    // Express: the cors() middleware handles OPTIONS automatically.
    // If you are handling CORS manually, add this:
    app.options('*', cors(corsOptions)); // enable pre-flight for all routes
    
    // The preflight response must include:
    Access-Control-Allow-Origin: https://app.example.com
    Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
    Access-Control-Allow-Headers: Content-Type, Authorization
    Access-Control-Max-Age: 86400   // cache the preflight for 24 hours
  5. 5

    Set Allow-Credentials for cookie-based auth

    If your API uses session cookies or requires the browser to send stored cookies, you must set Access-Control-Allow-Credentials: true and set a specific (non-wildcard) origin. The client also needs to set credentials: 'include' in the fetch call.

    javascript
    // Response header
    Access-Control-Allow-Credentials: true
    
    // Client side (fetch)
    fetch('https://api.example.com/me', {
        method: 'GET',
        credentials: 'include',  // send cookies cross-origin
    });
    
    // Client side (axios)
    axios.get('https://api.example.com/me', { withCredentials: true });
  6. 6

    Configure CORS in Laravel

    Laravel ships with a config/cors.php file and the HandleCors middleware registered globally. Edit the config — do not write a custom middleware.

    php
    // config/cors.php
    return [
        'paths'               => ['api/*'],
        'allowed_methods'     => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
        'allowed_origins'     => ['https://app.example.com', 'https://admin.example.com'],
        'allowed_origins_patterns' => [],
        'allowed_headers'     => ['Content-Type', 'Authorization', 'Accept'],
        'exposed_headers'     => [],
        'max_age'             => 86400,
        'supports_credentials'=> true,
    ];
  7. 7

    Install and configure cors in Express

    Install the cors package and apply it as early as possible in your middleware chain — before any routes.

    javascript
    npm install cors
    
    // app.js
    const cors = require('cors');
    
    const corsOptions = {
        origin: ['https://app.example.com', 'https://admin.example.com'],
        methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
        allowedHeaders: ['Content-Type', 'Authorization'],
        credentials: true,
        maxAge: 86400,
    };
    
    app.use(cors(corsOptions));
    app.options('*', cors(corsOptions)); // handle preflight for all routes
  8. 8

    Debug the "Access-Control-Allow-Origin" error in development

    During development your frontend runs on http://localhost:3000 and your API on http://localhost:8000. This is a cross-origin pair. Add http://localhost:3000 (or your dev port) to your allowed origins — do not add localhost without a port, as the port is part of the origin. Remove it before deploying to production.

    If the error persists after fixing the header, check the Network tab in browser devtools: look at the actual response headers on the failed request to confirm your server is sending the correct origin. A misconfigured reverse proxy often strips or overwrites CORS headers silently.

Tips & gotchas

  • CORS errors in the browser do not mean your API is secure — they only mean the browser refused to hand the response to JavaScript. A direct HTTP client (curl, Postman, server-side code) bypasses CORS entirely.
  • Keep your allowed origins list in an environment variable so it can differ between development, staging, and production without code changes.
  • Do not set <code>Access-Control-Allow-Headers: *</code> when credentials are enabled — wildcard headers are not supported with credentialed requests in many browsers.
  • A long <code>Access-Control-Max-Age</code> (86400 = 24 hours) reduces preflight overhead significantly for high-traffic APIs.

Wrapping up

CORS configuration is not complex once you understand that the browser is the enforcer, not your server. Keep your allowed origins list small and explicit, enable credentials only when needed, always handle preflight, and never use a wildcard on authenticated endpoints. Five minutes of correct configuration prevents hours of confused debugging.

#API #CORS #Security
Back to all guides

Need Help With Your Project?

Book a free 30-minute consultation to discuss your technical challenges and explore solutions together.