Skip to main content
Back to Blog

CORS Explained: Cross-Origin Resource Sharing Guide 2025

Why the browser blocks cross-origin requests, what preflight is, and how to set headers so your API works from the frontend.

Kashyap Thakar
9 min
CORSSecurityWeb DevelopmentAPI
CORS Explained: Cross-Origin Resource Sharing Guide 2025

If you've built web applications, you've likely encountered the dreaded CORS error: Access to fetch at 'https://api.example.com' from origin 'https://myapp.com' has been blocked by CORS policy. CORS (Cross-Origin Resource Sharing) is a security mechanism that browsers enforce, but it's often misunderstood. Let's demystify CORS and learn how to handle it correctly.

What is CORS?

CORS is a browser security feature that restricts web pages from making requests to a different domain than the one that served the web page. This is part of the Same-Origin Policy, which prevents malicious websites from accessing sensitive data from other sites.

An origin consists of three parts:

  • Protocol: http:// or https://
  • Domain: example.com
  • Port: 80, 443, 3000, etc. (default ports are implicit)

Two URLs have the same origin only if all three parts match. For example:

https://example.com/page1  → Same origin
https://example.com/page2  → Same origin
https://api.example.com    → Different origin (different subdomain)
http://example.com         → Different origin (different protocol)
https://example.com:8080   → Different origin (different port)

Why Does CORS Exist?

Without CORS, a malicious website could:

  • Make authenticated requests to your bank's API using cookies stored in your browser
  • Read responses from APIs that contain sensitive data
  • Perform actions on your behalf without your knowledge

CORS allows servers to explicitly permit cross-origin requests while maintaining security. It's an opt-in system—servers must explicitly allow cross-origin requests.

How CORS Works

When a browser makes a cross-origin request, it follows this process:

1. Simple Requests

For "simple" requests (GET, POST with certain content types), the browser makes the request directly and checks the response headers:

  • If Access-Control-Allow-Origin header matches the request origin, the request succeeds
  • If the header is missing or doesn't match, the browser blocks the response

2. Preflight Requests

For "non-simple" requests (PUT, DELETE, custom headers, JSON content type), the browser first sends an OPTIONS request (preflight) to check if the actual request is allowed:

// Browser sends preflight request
OPTIONS /api/users HTTP/1.1
Origin: https://myapp.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type

// Server responds with allowed methods/headers
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type
Access-Control-Max-Age: 86400

// If preflight succeeds, browser makes actual request
POST /api/users HTTP/1.1
Origin: https://myapp.com
Content-Type: application/json

CORS Headers Explained

Access-Control-Allow-Origin

Specifies which origins are allowed to access the resource. Can be a specific origin or * for all origins (not allowed with credentials).

Access-Control-Allow-Origin: https://myapp.com
// or
Access-Control-Allow-Origin: *

Access-Control-Allow-Methods

Specifies which HTTP methods are allowed for cross-origin requests. Used in preflight responses.

Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS

Access-Control-Allow-Headers

Specifies which headers can be used in the actual request. Used in preflight responses.

Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With

Access-Control-Allow-Credentials

When set to true, allows cookies and authorization headers to be sent with cross-origin requests. When using credentials, Access-Control-Allow-Origin cannot be *.

Access-Control-Allow-Credentials: true

Access-Control-Max-Age

Specifies how long (in seconds) the preflight response can be cached. Reduces the number of preflight requests.

Access-Control-Max-Age: 86400  // 24 hours

Common CORS Errors and Solutions

Error: "No 'Access-Control-Allow-Origin' header"

The server isn't sending the required CORS headers. Solution: Add CORS headers to your server response.

Error: "Credentials flag is true, but 'Access-Control-Allow-Credentials' is not 'true'"

You're sending credentials (cookies, auth headers) but the server hasn't explicitly allowed them. Solution: Set Access-Control-Allow-Credentials: true on the server.

Error: "Method PUT is not allowed by Access-Control-Allow-Methods"

The server's preflight response doesn't include the HTTP method you're trying to use. Solution: Add the method to Access-Control-Allow-Methods.

Error: "Request header field Authorization is not allowed"

The server's preflight response doesn't allow the custom header you're sending. Solution: Add the header to Access-Control-Allow-Headers.

Implementing CORS on the Server

Express.js with cors Middleware

const express = require('express');
const cors = require('cors');

const app = express();

// Allow all origins (development only)
app.use(cors());

// Allow specific origins
app.use(cors({
  origin: 'https://myapp.com',
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization']
}));

// Allow multiple origins
const allowedOrigins = [
  'https://myapp.com',
  'https://www.myapp.com',
  'http://localhost:3000'
];

app.use(cors({
  origin: function (origin, callback) {
    if (!origin || allowedOrigins.indexOf(origin) !== -1) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  credentials: true
}));

Manual CORS Headers (Any Framework)

// Handle preflight requests
app.options('/api/*', (req, res) => {
  res.header('Access-Control-Allow-Origin', 'https://myapp.com');
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  res.header('Access-Control-Allow-Credentials', 'true');
  res.sendStatus(200);
});

// Handle actual requests
app.use('/api', (req, res, next) => {
  res.header('Access-Control-Allow-Origin', 'https://myapp.com');
  res.header('Access-Control-Allow-Credentials', 'true');
  next();
});

Next.js API Routes

// pages/api/users.ts
import { NextApiRequest, NextApiResponse } from 'next';

export default function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  // Set CORS headers
  res.setHeader('Access-Control-Allow-Origin', 'https://myapp.com');
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  res.setHeader('Access-Control-Allow-Credentials', 'true');

  // Handle preflight
  if (req.method === 'OPTIONS') {
    return res.status(200).end();
  }

  // Handle actual request
  // ... your API logic
}

CORS Security Best Practices

1. Never Use * in Production

While Access-Control-Allow-Origin: * is convenient for development, it allows any website to make requests to your API. Always specify exact origins in production.

2. Validate Origins Dynamically

Don't hardcode origins. Use environment variables or a database to manage allowed origins. This makes it easier to add/remove origins without code changes.

3. Use Credentials Carefully

Only enable Access-Control-Allow-Credentials when you actually need to send cookies or auth headers. This reduces the attack surface.

4. Limit Allowed Methods and Headers

Only allow the HTTP methods and headers your API actually uses. Don't use wildcards like Access-Control-Allow-Methods: *.

5. Cache Preflight Responses

Use Access-Control-Max-Age to cache preflight responses. This reduces the number of OPTIONS requests and improves performance.

CORS vs Other Solutions

CORS vs JSONP

JSONP (JSON with Padding) was an older workaround for cross-origin requests. It only works for GET requests and has security vulnerabilities. CORS is the modern, secure solution.

CORS vs Proxy

You can avoid CORS entirely by proxying requests through your own server. However, this adds latency and server load. CORS is the preferred solution for direct client-to-API communication.

Debugging CORS Issues

When debugging CORS issues:

  • Check browser console: CORS errors are clearly logged with details
  • Inspect network tab: Look for preflight OPTIONS requests and their responses
  • Verify headers: Ensure all required CORS headers are present and correct
  • Test with curl: Use curl to verify server responses without browser CORS enforcement
  • Check credentials: If using credentials, ensure Access-Control-Allow-Credentials is set

Conclusion

CORS is a critical security mechanism that protects users from malicious websites. While it can be frustrating when first encountered, understanding how it works makes it much easier to implement correctly.

Remember: CORS is enforced by browsers, not servers. Server-side code doesn't need CORS—it's only needed when browsers make cross-origin requests. Always configure CORS headers on your API servers, validate origins carefully, and never use wildcards in production.

For testing API requests and inspecting CORS headers, check out our API Tester tool, which helps you debug CORS issues in real-time.

Part of the ThenCatch blog. Learn more about us or browse more guides.