Skip to content
ADevGuide Logo ADevGuide
Go back

What Is CORS? Complete Beginner Explanation

By Pratik Bhuite | 21 min read

Hub: Web Fundamentals / Networking and Protocols

Series: API and Backend Basics Series

Last verified: Mar 30, 2026

Part 9 of 10 in the API and Backend Basics Series

Key Takeaways

On this page
Reading Comfort:

What Is CORS? Complete Beginner Explanation

CORS is one of the first browser security features that confuses backend and frontend developers. The browser says a request was blocked, Postman works fine, the API might even return 200, and suddenly it feels like the network is lying to you.

This guide explains what CORS really is, why browsers enforce it, how preflight requests work, and how to configure the right headers without accidentally exposing your API too broadly. If the overall request-response lifecycle is still fuzzy, start with How APIs Work: A Simple Guide for Beginners and then come back here.

Table of Contents

Open Table of Contents

Quick Definition

CORS stands for Cross-Origin Resource Sharing.

In plain English:

  1. A browser page loaded from one origin tries to read data from a different origin.
  2. The browser applies the same-origin policy by default.
  3. The server can opt in by sending specific HTTP headers that tell the browser the cross-origin read is allowed.

The important part is this:

CORS is mostly a browser security rule about whether frontend JavaScript can read a cross-origin response. It is not a replacement for authentication, authorization, or CSRF protection.

What “Origin” Means in the Browser

An origin is the combination of:

  1. Scheme (http or https)
  2. Host (app.example.com)
  3. Port (443, 3000, 8080)

Examples:

  • https://app.example.com -> https://api.example.com is cross-origin because the host is different.
  • http://app.example.com -> https://app.example.com is cross-origin because the scheme is different.
  • https://app.example.com:3000 -> https://app.example.com:443 is cross-origin because the port is different.

That second case is why transport details matter. If you have not read it yet, HTTP vs HTTPS: What’s the Difference? is useful context because the scheme is part of the origin, not just a security decoration.

Why Browsers Block Cross-Origin Reads

Browsers do not block cross-origin requests randomly. They do it because allowing any website to read any other site’s responses would create major security problems.

Imagine this attack:

  1. You are logged in to bank.example.com.
  2. You visit a malicious website in another tab.
  3. That site runs JavaScript that silently calls the bank API with your browser cookies.
  4. If the browser exposed the bank response to that malicious script by default, the attacker could read private account data.

The same-origin policy exists to stop that kind of cross-site data leak.

CORS is the controlled exception. It lets the server say, “Yes, this particular other origin may read this response from the browser.”

One subtle point matters in production:

  • CORS controls browser access to the response.
  • It does not mean the server was never reached.
  • For some simple cross-origin requests, the browser may send the request and then block JavaScript from reading the response.
  • For preflighted requests, the browser may stop before the real request if the preflight check fails.

How CORS Works Step by Step

flowchart TD
    A[Browser App on https://app.example.com] --> B[Send fetch request to https://api.example.com]
    B --> C{Cross-origin request?}
    C -->|No| D[Normal same-origin flow]
    D --> A
    C -->|Yes| E{Needs preflight?}
    E -->|No| F[Send actual request with Origin header]
    F --> G[API responds with CORS headers]
    G --> H{Origin allowed?}
    H -->|Yes| I[Browser exposes response to JavaScript]
    H -->|No| J[Browser blocks response access]
    I --> A
    J --> A
    E -->|Yes| K[Browser sends OPTIONS preflight]
    K --> L[API returns allowed methods headers origins]
    L --> M{Preflight approved?}
    M -->|No| J
    M -->|Yes| F

    classDef cors fill:#e8f0fe,stroke:#1a73e8,stroke-width:2px,color:#000000;
    class A,B,C,D,E,F,G,H,I,J,K,L,M cors;

The browser usually adds an Origin header automatically on cross-origin requests:

Origin: https://app.example.com

If the API wants to allow that origin, it answers with a header such as:

Access-Control-Allow-Origin: https://app.example.com

If the response headers match what the browser expects, the browser lets your JavaScript read the response. If not, the browser blocks access and surfaces a CORS error in DevTools.

Simple Requests vs Preflight Requests

Not every cross-origin request triggers the extra OPTIONS round trip.

Simple requests

A request is usually simple when it looks close to what normal HTML forms could already send:

  1. Commonly GET, HEAD, or POST
  2. No unusual custom request headers
  3. Only simple content types such as form-encoded or plain text

Example:

fetch("https://api.example.com/products");

In that case, the browser can send the request directly and then decide whether the response is readable based on the returned CORS headers.

Preflight requests

A preflight happens before the real request when the browser needs to check whether the server allows a more sensitive cross-origin operation.

Common triggers:

  1. Methods like PUT, PATCH, or DELETE
  2. Custom headers such as Authorization or X-Request-ID
  3. Content-Type: application/json

Example preflight:

OPTIONS /orders/ord_42 HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: authorization, content-type

Example approval:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400

If you want a refresher on why DELETE, PATCH, and other methods carry different semantics, HTTP Methods Explained: GET vs POST vs PUT vs DELETE connects method choice back to CORS behavior.

Common CORS Headers Explained

These are the headers developers see most often.

Access-Control-Allow-Origin

This is the core decision header. It tells the browser which origin is allowed to read the response.

Examples:

Access-Control-Allow-Origin: https://app.example.com

or

Access-Control-Allow-Origin: *

Use * only for truly public, non-credentialed resources. It is a bad default for private APIs.

Access-Control-Allow-Methods

Used in preflight responses to list allowed methods.

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

Access-Control-Allow-Headers

Used in preflight responses to say which custom request headers are allowed.

Access-Control-Allow-Headers: Authorization, Content-Type, X-Request-ID

Access-Control-Allow-Credentials

Tells the browser that credentials such as cookies or HTTP auth may be included.

Access-Control-Allow-Credentials: true

This setting changes the risk profile, so it should be used narrowly.

Access-Control-Max-Age

Lets the browser cache successful preflight results for a while so it does not need to repeat the OPTIONS request every time.

Access-Control-Max-Age: 86400

CORS with Cookies and Credentials

Credentials make CORS more sensitive because now the browser may be sending cookies, client certificates, or HTTP auth automatically.

When credentials are involved:

  1. The frontend must opt in, for example credentials: "include" in fetch.
  2. The server must return Access-Control-Allow-Credentials: true.
  3. The server must return a specific allowed origin, not *.

Example:

await fetch("https://api.example.com/account", {
  method: "GET",
  credentials: "include",
});

This is also where developers often mix up CORS and identity. CORS does not decide who the user is. It only decides whether the browser exposes the response to frontend code from another origin. Identity and permission checks still belong to your auth layer. If you want that distinction clearly separated, Authentication vs Authorization: What’s the Difference? is the right companion post.

Why Postman Works When the Browser Fails

This is one of the most common beginner questions.

Postman is not a browser page running inside browser security boundaries. It can send HTTP requests directly without enforcing the same-origin policy the way a browser does for frontend JavaScript.

That means:

  1. Your API can work perfectly in Postman.
  2. The same API can fail in the browser with a CORS error.
  3. The problem is not always the endpoint logic. It is often the missing CORS response headers.

This is also why server-to-server calls usually do not care about CORS. A backend calling another backend is not blocked by browser-origin rules.

Common CORS Mistakes Developers Make

1. Treating CORS as an authentication system

CORS is not login and not authorization. It does not stop an allowed origin from sending a bad request, and it does not prove who the caller is.

2. Returning Access-Control-Allow-Origin: * on private APIs

If an API is user-specific or uses credentials, a wildcard is too broad.

3. Forgetting the OPTIONS path

Teams add Access-Control-Allow-Origin to normal responses but never handle preflight requests, so PUT, PATCH, DELETE, or auth-heavy calls still fail.

4. Allowing every origin by reflecting whatever arrives

Blindly copying the incoming Origin header without an allowlist turns CORS into an accidental open policy. That is especially risky when credentials are enabled.

5. Debugging only in application logs

Many CORS failures look invisible at the app level because the browser blocks access after the response. You have to check the browser network panel and response headers, not just server logs.

6. Assuming CORS is the only browser security concern

CORS solves one problem: controlled cross-origin reads. It does not replace CSRF protection, secure cookies, or HTTPS. For the broader path around those web boundaries, browse the Web Fundamentals hub and the API tag archive.

Real-World Example: Frontend App Calling an API

Imagine this deployment:

  1. Frontend React app: https://app.adevguide.com
  2. Backend API: https://api.adevguide.com
  3. User login uses a cookie-based session

From the browser’s perspective, this is cross-origin because the frontend and API are on different subdomains.

Typical request:

await fetch("https://api.adevguide.com/me", {
  credentials: "include",
  headers: {
    "Content-Type": "application/json",
  },
});

Because credentials and JSON are involved, the browser will usually preflight first. The API needs to answer something like:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.adevguide.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 600

A minimal Node-style allowlist implementation might look like this:

const allowedOrigins = new Set([
  "https://app.adevguide.com",
  "https://admin.adevguide.com",
]);

app.use((req, res, next) => {
  const origin = req.headers.origin;

  if (origin && allowedOrigins.has(origin)) {
    res.setHeader("Access-Control-Allow-Origin", origin);
    res.setHeader("Access-Control-Allow-Credentials", "true");
    res.setHeader(
      "Access-Control-Allow-Methods",
      "GET, POST, PUT, PATCH, DELETE, OPTIONS"
    );
    res.setHeader(
      "Access-Control-Allow-Headers",
      "Authorization, Content-Type"
    );
  }

  if (req.method === "OPTIONS") {
    res.status(204).end();
    return;
  }

  next();
});

Why this design is safer:

  1. Only known frontend origins are allowed.
  2. Credentials are enabled only for those approved origins.
  3. Preflight requests are handled explicitly instead of failing mysteriously.
  4. The policy is narrow enough to evolve as environments change.

Interview Questions

1. What problem does CORS actually solve?

CORS solves a browser security problem, not a generic network problem. It gives servers a way to explicitly allow frontend JavaScript from one origin to read responses from another origin.

In interviews, I would say it is a controlled exception to the same-origin policy. Without it, browsers would expose too much cross-site data. With it, servers can allow selected cross-origin access instead of opening everything.

2. Why does Postman succeed while browser fetch() fails with a CORS error?

Because Postman is not enforcing browser same-origin rules on behalf of a web page. It is just an HTTP client. The browser, by contrast, is protecting the user against scripts from one site reading sensitive responses from another site.

That is why a backend can be healthy, reachable, and return 200, while the frontend still sees a CORS failure. The browser is blocking access to the response, not claiming the server is down.

3. What triggers a preflight request?

A preflight is triggered when the browser sees a cross-origin request that is more sensitive than a simple form-style request. Common triggers are methods like PUT, PATCH, and DELETE, custom headers such as Authorization, or content types like application/json.

The key idea is that the browser wants server approval before sending the real request. That approval happens through an OPTIONS request.

4. Why can you not combine Access-Control-Allow-Origin: * with credentials?

Because credentials make the request user-specific and more sensitive. If the browser allowed credentialed cross-origin reads from every origin, any website could potentially read authenticated responses on behalf of the user.

So when credentials are involved, the server has to name specific allowed origins and opt in deliberately. A wildcard is too broad for that case.

5. Is CORS a replacement for authentication or authorization?

No. CORS only affects whether the browser exposes a cross-origin response to frontend JavaScript. Authentication still proves identity, and authorization still decides whether that identity can access a resource.

If your API skips auth checks but has a strict CORS policy, it is still insecure. CORS is a browser access rule, not an identity system.

6. Why do some CORS failures still hit the backend?

Because not every failure happens before the request leaves the browser. For simple requests, the browser may send the request, receive the response, and then refuse to expose that response to JavaScript because the CORS headers are missing or wrong.

For preflighted requests, the failure can happen earlier, because the browser may block the real request if the preflight response does not approve it. That difference is important when debugging logs and traces.

Conclusion

CORS becomes much easier once you stop treating it as mysterious browser behavior and start treating it as a policy handshake:

  1. The browser declares the requesting origin.
  2. The server decides whether that origin may read the response.
  3. The browser enforces the answer.

For beginners, the main upgrade is simple: do not “fix CORS” by opening everything. Start with the minimum allowed origins, handle preflight correctly, and keep CORS separate from authentication and authorization logic.

References

  1. MDN: Cross-Origin Resource Sharing (CORS)
    https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS
  2. MDN: Preflight request
    https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request
  3. MDN: Same-origin policy
    https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy
  4. MDN: Cross-Origin Resource Sharing (CORS) configuration
    https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implementation_guides/CORS

YouTube Videos

  1. “CORS in 100 Seconds” - Fireship
    https://www.youtube.com/watch?v=4KHiSt0oLJ0
  2. “Learn CORS In 6 Minutes” - Web Dev Simplified
    https://www.youtube.com/watch?v=PNtFSVU-YTI
  3. “What is CORS? Blocked by CORS policy error explained” - Dave Gray
    https://www.youtube.com/watch?v=1iOeoRCUD4M

Share this post on:

Next in Series

Continue through the API and Backend Basics Series with the next recommended article.

Related Posts

Keep Learning with New Posts

Subscribe through RSS and follow the project to get new series updates.

Was this guide helpful?

Share detailed feedback

Previous Post
REST API Error Handling Best Practices: Beginner Guide
Next Post
What Is Agentic AI? The Next Evolution After ChatGPT