WEB API's Lesson 22 – CORS | Dataplexa
Web APIs · Lesson 22

CORS

Understand browser security restrictions and learn how to configure Cross-Origin Resource Sharing for secure API access.

Netflix serves video content from dozens of different domains. Their web player runs on netflix.com but fetches data from api-global.netflix.com, content from assets.nflxext.com, and analytics from ichnaea-web.netflix.com. Without proper CORS configuration, your browser would block every single request and Netflix would be a blank screen.

CORS stands for Cross-Origin Resource Sharing, and it solves one of the web's most fundamental security challenges. When JavaScript code running on one domain tries to make HTTP requests to another domain, browsers step in with a security policy called the Same-Origin Policy.

This policy exists because without it, malicious websites could silently steal your data from other sites you're logged into. Imagine visiting evil-site.com, which then secretly makes requests to your-bank.com using your logged-in session. CORS prevents this nightmare scenario while still allowing legitimate cross-domain API communication.

But modern web applications need cross-domain requests all the time. Your frontend might run on localhost:3000 during development while hitting an API on localhost:8000. In production, your app might be on app.company.com while your API lives on api.company.com. CORS provides the controlled mechanism that makes this possible.

1
Browser notices request to different origin
2
Browser sends preflight OPTIONS request (for complex requests)
3
Server responds with CORS headers
4
Browser evaluates headers against request
5
Browser allows or blocks the actual request

What Counts as a Different Origin

Origins have three components that must all match for requests to be considered "same-origin." Change any one piece and you've crossed into CORS territory.

The protocol matters. https://api.example.com and http://api.example.com are different origins. The domain matters. api.example.com and cdn.example.com are different origins. The port matters. localhost:3000 and localhost:8000 are different origins.

This strictness catches developers off guard. You might think subdomains would be treated as the same origin, but they're not. Moving from HTTP to HTTPS during development breaks things. Even implied ports count — port 80 for HTTP and 443 for HTTPS are still different origins if you specify them explicitly.

Same Origin

https://app.example.com
https://app.example.com/api
https://app.example.com:443

Different Origin

https://api.example.com
http://app.example.com
https://app.example.com:8080

Simple vs Preflighted Requests

Not all cross-origin requests trigger the same CORS behavior. Browsers classify requests into two categories based on their complexity and potential for causing server side effects.

Simple requests get sent immediately with CORS headers checked afterward. These include GET, HEAD, and POST requests with basic content types like application/x-www-form-urlencoded or text/plain. The browser sends the request and then validates the response headers to decide whether JavaScript should see the result.

Preflighted requests trigger an OPTIONS request first. This happens with PUT, DELETE, PATCH methods, custom headers like Authorization, or JSON content types. The browser asks the server "would you allow this request?" before sending the actual data.

The preflight system exists because servers built before CORS might not expect complex cross-origin requests. A legacy API that never anticipated DELETE requests from web browsers shouldn't suddenly receive them without warning. The preflight gives servers a chance to explicitly opt-in to these interactions.

Simple Requests

GET, HEAD, POST methods

Basic content types only

No custom headers

Preflighted Requests

PUT, DELETE, PATCH methods

JSON content type

Authorization headers

Essential CORS Headers

CORS communication happens entirely through HTTP headers. The server uses these headers to tell browsers exactly what cross-origin requests it will accept and under what conditions.
Header
Function
Control Level
Preflight
Security Impact
Header Purpose APIForge Usage
Access-Control-Allow-Origin Specifies which origins can access the resource Allow dashboard.apiforge.com to call api.apiforge.com
Access-Control-Allow-Methods Lists HTTP methods permitted in cross-origin requests Enable GET, POST, PUT, DELETE for API endpoints
Access-Control-Allow-Headers Defines which headers the client can send Accept Authorization and Content-Type headers
Access-Control-Allow-Credentials Controls whether cookies/auth can be included Allow authenticated requests from frontend apps
Access-Control-Max-Age Caches preflight responses to reduce requests Cache preflight results for 24 hours

Access-Control-Allow-Origin is the most critical header. Set it to * and you allow requests from any domain — convenient for development but dangerous in production. Set it to a specific domain and you lock down access to just that origin. You can only specify one origin per response, not a list.

Access-Control-Allow-Credentials deserves special attention. When set to true, it allows requests to include cookies, authorization headers, and client certificates. But this comes with a restriction — you cannot use a wildcard origin when credentials are allowed. The browser enforces this security rule strictly.

Implementing CORS in Practice

The APIForge Backend team needs to configure CORS for their new developer dashboard. The React frontend runs on dashboard.apiforge.com and needs to make authenticated API calls to api.apiforge.com for user data, project metrics, and billing information.
// APIForge API server CORS configuration
const express = require('express');
const cors = require('cors');
const app = express();

const corsOptions = {
  origin: ['https://dashboard.apiforge.com', 'https://app.apiforge.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key'],
  credentials: true,
  maxAge: 86400
};

app.use(cors(corsOptions));

app.get('/api/user/profile', (req, res) => {
  res.json({ id: 1, name: 'Sarah Chen', role: 'Backend Engineer' });
});
HTTP/1.1 200 OK Access-Control-Allow-Origin: https://dashboard.apiforge.com Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS Access-Control-Allow-Headers: Content-Type, Authorization, X-API-Key Access-Control-Allow-Credentials: true Access-Control-Max-Age: 86400 Content-Type: application/json { "id": 1, "name": "Sarah Chen", "role": "Backend Engineer" }

What just happened?

The Express server configured CORS to accept requests from two specific APIForge domains. When the dashboard makes requests, these headers tell the browser the cross-origin call is permitted.

The credentials: true setting allows the frontend to include authentication cookies or Authorization headers. The maxAge value caches preflight responses for 24 hours to improve performance.

Try this: Check your browser's Network tab during a cross-origin request — you'll see the preflight OPTIONS request and the CORS headers in the response.

This configuration solves the immediate problem but creates new ones for development. Local development typically runs on localhost:3000, which isn't in the allowed origins list. The team needs environment-specific CORS settings that are permissive locally but strict in production.

// Environment-aware CORS configuration
const isDevelopment = process.env.NODE_ENV === 'development';

const corsOptions = {
  origin: function (origin, callback) {
    // Allow requests with no origin (mobile apps, Postman)
    if (!origin) return callback(null, true);
    
    const allowedOrigins = isDevelopment 
      ? ['http://localhost:3000', 'http://localhost:3001']
      : ['https://dashboard.apiforge.com', 'https://app.apiforge.com'];
    
    if (allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('CORS policy violation'));
    }
  },
  credentials: true
};
// Development request from localhost:3000 HTTP/1.1 200 OK Access-Control-Allow-Origin: http://localhost:3000 Access-Control-Allow-Credentials: true // Production request from unknown origin HTTP/1.1 200 OK Content-Type: text/html Error: CORS policy violation

What just happened?

The origin function runs for each request, checking the requesting domain against environment-specific allowed lists. Development allows localhost origins while production restricts access to official APIForge domains.

The function also handles requests with no origin header — these come from mobile apps, API clients like Postman, or server-to-server requests that don't trigger CORS at all.

Try this: Test your CORS configuration with curl or Postman — these tools don't send Origin headers, so they bypass CORS entirely.

Common CORS Pitfalls

CORS errors are notoriously confusing because browsers provide vague error messages that don't explain the actual problem. Understanding the most common failure modes saves hours of debugging time.

The "has been blocked by CORS policy" error appears in dozens of variations, but the root cause usually falls into one of five categories. Missing Access-Control-Allow-Origin headers. Mismatched origins in development vs production. Attempting to use wildcards with credentials. Forgetting to handle preflight OPTIONS requests. Trying to read response data from failed CORS requests.

One particularly frustrating scenario happens when your API returns a 500 error but your JavaScript sees a CORS error instead. The server error triggers first, but without proper CORS headers in the error response, the browser blocks access to the error details. Your code can't even read the status code or error message.

CORS Credential Trap

Setting Access-Control-Allow-Origin to * while Access-Control-Allow-Credentials is true causes an immediate CORS failure. Browsers reject this combination as a security violation. You must specify exact origins when allowing credentials.

The preflight cache creates another class of problems. Browsers cache successful preflight responses based on the Max-Age header, but they don't cache failures. This means a misconfigured server might work intermittently — failing on the first request when preflight is required, then succeeding on subsequent requests when preflight is cached.

Development environments compound these issues. Your localhost frontend making requests to a local API server works fine, but deploying to staging breaks everything because the origins change. HTTPS certificates, subdomain configurations, and port numbers all affect CORS behavior in ways that aren't obvious during local development.

CORS Security Considerations

CORS exists to prevent malicious websites from making unauthorized requests to other domains, but configuring it incorrectly can create security vulnerabilities rather than preventing them.

The wildcard origin (*) disables CORS protection entirely. Any website can make requests to your API, potentially exposing user data or triggering unwanted actions. This might seem acceptable for public APIs, but even read-only endpoints can leak information about API structure, rate limits, or error handling that assists attackers.

Allowing credentials amplifies these risks dramatically. Authenticated requests carry user context — session cookies, authorization tokens, or client certificates that identify specific users. A malicious site that tricks users into visiting while logged into your application could perform actions on their behalf.

The null origin presents a particular challenge. Browsers send Origin: null for requests from file:// URLs, sandboxed iframes, or redirected requests. Some developers allow null origins to support local testing, but this also allows data: URLs and other potentially dangerous contexts.

Dynamic Origin Validation

Instead of hardcoding allowed origins, consider validating them against a database or configuration service. This allows you to add new origins without deploying code changes, but be cautious about who can modify the allowed origins list.

Subdomain wildcards create another security consideration. While CORS doesn't support subdomain wildcards directly, some developers implement custom logic to allow any subdomain of their domain. This works until someone registers a similar domain name or finds a way to create unauthorized subdomains.

The principle of least privilege applies strongly to CORS configuration. Only allow the specific origins, methods, and headers your application actually needs. Audit these settings regularly as your application evolves and remove any permissions that are no longer required.

Debugging CORS Issues

Effective CORS debugging requires understanding what browsers actually send and receive, not just what your JavaScript code attempts to do.

Browser developer tools reveal the complete CORS conversation. Open the Network tab and look for OPTIONS requests — these are your preflights. Failed preflight requests prevent the actual request from ever being sent. Successful preflights show you exactly which headers the server returned and whether they match what your request needs.

The Console tab shows CORS errors, but these messages are often misleading. A "CORS policy" error might actually be caused by a network failure, a 500 server error, or even a typo in your URL. The Network tab shows what actually happened at the HTTP level.

// APIForge debugging utility for CORS issues
async function debugCorsRequest(url, options = {}) {
  console.log('🔍 CORS Debug Request:', {
    url,
    method: options.method || 'GET',
    headers: options.headers || {},
    credentials: options.credentials || 'same-origin'
  });

  try {
    const response = await fetch(url, options);
    console.log('✅ CORS Success:', {
      status: response.status,
      headers: Object.fromEntries(response.headers.entries())
    });
    return response;
  } catch (error) {
    console.error('❌ CORS Failure:', {
      message: error.message,
      type: error.constructor.name
    });
    throw error;
  }
}

// Test APIForge API endpoint
debugCorsRequest('https://api.apiforge.com/user/profile', {
  method: 'GET',
  headers: { 'Authorization': 'Bearer token123' },
  credentials: 'include'
});
🔍 CORS Debug Request: { url: "https://api.apiforge.com/user/profile", method: "GET", headers: { "Authorization": "Bearer token123" }, credentials: "include" } ✅ CORS Success: { status: 200, headers: { "access-control-allow-origin": "https://dashboard.apiforge.com", "access-control-allow-credentials": "true", "content-type": "application/json" } }

What just happened?

The debug utility logs both the request configuration and the response headers, making CORS issues immediately visible. You can see whether the server is returning the expected CORS headers and whether they match your request requirements.

This approach catches common problems like missing credentials in the request, incorrect origin headers, or server-side CORS configuration errors before they become confusing browser error messages.

Try this: Use this pattern in your development environment to verify CORS configuration before deploying to production.

Server-side logging complements client-side debugging. Log incoming OPTIONS requests and the CORS headers you're sending in response. Many CORS issues stem from servers that handle regular requests correctly but fail to process preflight requests or return the wrong headers.

Testing with curl bypasses CORS entirely and helps isolate server-side issues from browser-side restrictions. If curl works but your browser doesn't, you know the problem is in CORS configuration rather than API functionality.

Quiz

1. The APIForge frontend at https://app.apiforge.com needs to make requests to https://api.apiforge.com. What must happen for these requests to work?

2. When a browser encounters a cross-origin PUT request with JSON data, what does it do first?

3. Your APIForge API needs to accept authenticated requests from multiple frontend applications. Why can't you set Access-Control-Allow-Origin to * and Access-Control-Allow-Credentials to true?

Up Next
Rate Limiting
APIForge implements request throttling to protect their API infrastructure from abuse and ensure fair usage across all clients.