TL;DR: CORS (Cross-Origin Resource Sharing) is a browser security feature that blocks web pages from making requests to a different domain, port, or protocol than the one that served the page. The fix is always on the server side: add the correct Access-Control-Allow-Origin header. AI tools frequently forget this, which is why CORS errors are the #1 surprise for vibe coders.

Why AI Coders Need to Know This

CORS errors are arguably the most common "it works in development but breaks in the browser" issue in all of web development. According to Stack Overflow data, CORS-related questions have been viewed over 10 million times. For AI-assisted developers, it is even worse because the AI often builds your frontend and backend separately without configuring them to talk to each other properly.

Here is the typical vibe coder experience:

  1. You ask Cursor to build a React frontend that fetches data from your Express API.
  2. The AI generates both pieces perfectly — clean code, proper error handling, great UI.
  3. You run both servers. The frontend is on localhost:3000. The API is on localhost:5000.
  4. You open the browser. Red console error: Access to fetch at 'http://localhost:5000/api/data' from origin 'http://localhost:3000' has been blocked by CORS policy.
  5. You are confused because the code looks correct. It is. The browser is blocking the request for security reasons.

CORS is not a bug in your code. It is a security boundary the browser enforces. Understanding it takes about 10 minutes and saves you hours of confused debugging over the lifetime of every project you build.

What Is an "Origin"?

Before CORS makes sense, you need to understand what "origin" means. An origin is the combination of three things:

  • Protocol: http or https
  • Domain: localhost, example.com, api.mysite.com
  • Port: 3000, 5000, 443 (default for HTTPS)

If any of these three differ between the page making the request and the server receiving it, the request is cross-origin and CORS rules apply.

Same Origin ✅

http://localhost:3000http://localhost:3000/api

Same protocol, domain, and port. No CORS issue.

Cross-Origin ❌

http://localhost:3000http://localhost:5000/api

Different port. Browser blocks this unless server allows it.

This is why AI-generated apps with separate frontend and backend servers almost always hit CORS problems. The two servers have different ports, which means different origins.

Real Scenario

You are building a task management app. You asked Claude Code to create a Next.js frontend and an Express API backend. Everything works in isolation — the API responds correctly when you hit it with curl, and the frontend renders beautifully. But when the frontend tries to fetch tasks from the API, the browser blocks it.

Prompt I Would Type

Build a task management app with:
- Express.js backend API on port 5000 with GET /api/tasks and POST /api/tasks
- Next.js frontend on port 3000 that fetches and displays tasks
- Make sure the frontend can actually talk to the backend (handle CORS)
- Show me both the server setup and the fetch code

Notice the explicit "handle CORS" instruction. Without it, most AI tools will generate the frontend and backend correctly but forget to configure CORS. Adding that one line to your prompt saves a debugging round-trip every time.

What AI Generated

Here is the Express backend with CORS properly configured:

// server.js — Express API with CORS
const express = require('express');
const cors = require('cors'); // The CORS middleware package

const app = express();

// Allow requests from your frontend origin
app.use(cors({
  origin: 'http://localhost:3000',  // Only allow your frontend
  methods: ['GET', 'POST'],         // Only these HTTP methods
  credentials: true                  // Allow cookies if needed
}));

app.use(express.json()); // Parse JSON request bodies

let tasks = [
  { id: 1, title: 'Learn what CORS is', done: true },
  { id: 2, title: 'Fix the CORS error', done: false }
];

app.get('/api/tasks', (req, res) => {
  res.json(tasks);
});

app.post('/api/tasks', (req, res) => {
  const task = {
    id: tasks.length + 1,
    title: req.body.title,
    done: false
  };
  tasks.push(task);
  res.status(201).json(task);
});

app.listen(5000, () => {
  console.log('API running on http://localhost:5000');
});

And here is the frontend fetch code:

// In your Next.js component
async function fetchTasks() {
  const response = await fetch('http://localhost:5000/api/tasks', {
    credentials: 'include' // Send cookies if CORS allows it
  });

  if (!response.ok) {
    throw new Error(`Failed to fetch: ${response.status}`);
  }

  return response.json();
}

The critical line is app.use(cors({ origin: 'http://localhost:3000' })). This tells Express to add the Access-Control-Allow-Origin: http://localhost:3000 header to every response. When the browser sees that header, it knows the server explicitly permits requests from that origin and lets the data through.

Understanding Each Part

How CORS actually works

CORS is a negotiation between the browser and the server. Here is the sequence:

  1. Your frontend JavaScript calls fetch('http://localhost:5000/api/tasks').
  2. The browser notices the request goes to a different origin than the page.
  3. The browser sends the request but checks the response headers before giving the data to your JavaScript.
  4. If the response includes Access-Control-Allow-Origin matching your frontend's origin, the browser allows the data through.
  5. If the header is missing or does not match, the browser blocks the response. Your JavaScript gets a network error.

The key insight: the server still receives and processes the request. CORS is not a firewall. The data goes to the server and the server responds. But the browser refuses to hand the response to your JavaScript unless the server's headers explicitly permit it.

The Access-Control-Allow-Origin header

This is the most important CORS header. It tells the browser which origins are allowed to read the response.

// Allow one specific origin
Access-Control-Allow-Origin: http://localhost:3000

// Allow any origin (less secure, fine for public APIs)
Access-Control-Allow-Origin: *

Using * (wildcard) allows any website to call your API. This is appropriate for public APIs but dangerous for private ones. If your API handles user data or authentication, always specify the exact origin.

Preflight requests

For certain types of requests, the browser sends an automatic OPTIONS request before the real one. This is called a "preflight." It happens when:

  • The request uses a method other than GET, HEAD, or POST
  • The request includes custom headers (like Authorization)
  • The request sends JSON with Content-Type: application/json

The preflight asks the server: "Would you accept this type of request from this origin?" If the server responds correctly, the browser sends the actual request. If not, the browser blocks it and you see a CORS error in the console.

This is why POST requests with JSON bodies fail more often than simple GET requests — the JSON content type triggers a preflight that many servers do not handle.

The cors npm package

In the Express ecosystem, the cors package handles all of this automatically. You configure it once and it adds the correct headers to every response, including preflight responses.

npm install cors

Without this package, you would need to manually set headers on every route:

// Manual CORS headers (the hard way)
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', 'http://localhost:3000');
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  if (req.method === 'OPTIONS') {
    return res.sendStatus(204);
  }
  next();
});

The cors package does exactly this but handles edge cases, multiple origins, and credentials correctly. Use the package.

What AI Gets Wrong About CORS

Forgetting CORS entirely

The #1 AI CORS mistake is simply not configuring it. The AI generates a clean frontend and a clean backend, tests them independently, and declares success. But it never considers that the browser will block cross-origin requests. Always ask explicitly: "Make sure CORS is configured between the frontend and backend."

Using wildcard with credentials

// ❌ This will NOT work
app.use(cors({
  origin: '*',
  credentials: true  // Cannot combine * with credentials
}));

The CORS spec explicitly forbids using * as the origin when credentials (cookies, auth headers) are involved. AI generates this combination frequently. The fix: specify the exact origin instead of *.

Missing preflight handling

If AI sets CORS headers manually instead of using the cors package, it often forgets to handle the OPTIONS preflight request. The GET request works, but the POST request with JSON fails because the preflight gets a 404.

Hardcoded localhost in production

AI sets origin: 'http://localhost:3000' during development and never updates it for production. When you deploy, the frontend is at https://myapp.com but the server still only allows localhost. Use environment variables:

app.use(cors({
  origin: process.env.FRONTEND_URL || 'http://localhost:3000'
}));

Trying to fix CORS on the frontend

AI sometimes adds headers to the fetch() request on the frontend, thinking that will solve the CORS problem. It will not. CORS is enforced by the browser based on the server's response headers. The fix is always on the server side.

The CORS Rule

CORS errors are always fixed on the server, never on the client. If you are adding headers to your fetch() call to fix CORS, you are solving the wrong problem.

How to Debug CORS With AI

Read the actual error message

Chrome's CORS error messages are surprisingly specific. They tell you exactly which header is missing or misconfigured. Copy the full error message — do not paraphrase it — and paste it into your AI tool.

Check the Network tab

Open DevTools → Network → find the failed request. Look at:

  • Request headers: What origin is the browser sending?
  • Response headers: Is Access-Control-Allow-Origin present? Does it match?
  • Is there a preflight? Look for an OPTIONS request to the same URL right before the real request.

Test without the browser

Use curl or Postman to call the API directly. If the API works fine outside the browser, the issue is purely CORS headers — the server is working, it just is not telling the browser it is allowed to share the response.

curl -v http://localhost:5000/api/tasks

Look for the Access-Control-Allow-Origin header in the response. If it is missing, that is your problem.

The debugging prompt

Debug Prompt

I'm getting this CORS error in Chrome:
[paste exact error message]

My frontend runs on: http://localhost:3000
My backend runs on: http://localhost:5000
Here's my server setup: [paste server code]

What CORS configuration do I need to add to fix this?

Framework-specific CORS setup

  • Express: npm install corsapp.use(cors({ origin: '...' }))
  • Next.js API routes: Add headers in next.config.js or set them in the API route handler
  • Flask: pip install flask-corsCORS(app, origins=['...'])
  • Django: pip install django-cors-headers → add to MIDDLEWARE and CORS_ALLOWED_ORIGINS
  • Vercel/Netlify: Configure in vercel.json or _headers file

When asking AI to set up a project, always mention your specific framework so it uses the correct CORS configuration approach.

CORS in Production

Development CORS and production CORS are different problems. In development, both servers are on localhost with different ports. In production, you typically have one of these setups:

Same origin (no CORS needed)

If your frontend and API are served from the same domain — for example, Next.js with API routes — there is no cross-origin request and CORS does not apply. This is the simplest architecture.

Subdomain API

Frontend at https://myapp.com, API at https://api.myapp.com. These are different origins, so you need CORS. Set your origin to https://myapp.com.

Separate domains

Frontend at https://myapp.com, API at https://myapi.io. Fully cross-origin. CORS required with explicit origin configuration.

Multiple allowed origins

const allowedOrigins = [
  'https://myapp.com',
  'https://staging.myapp.com',
  process.env.NODE_ENV === 'development' && 'http://localhost:3000'
].filter(Boolean);

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

What to Learn Next

CORS sits at the intersection of frontend, backend, and security. Now that you understand it, these related topics become much clearer:

Next Step

The next time AI generates a project with separate frontend and backend, add "configure CORS between them" to your prompt before you start. That one sentence saves the most common debugging session in all of web development.

FAQ

CORS stands for Cross-Origin Resource Sharing. It is a browser security mechanism that controls which websites can make requests to which servers by checking HTTP response headers.

Because http://localhost:3000 and http://localhost:5000 are different origins — they have different ports. The browser treats them as separate websites and blocks cross-origin requests unless the server explicitly allows it with CORS headers.

CORS is enforced by the browser (frontend side) but must be fixed on the server (backend side). The server needs to include the correct Access-Control-Allow-Origin header in its responses. No amount of frontend code changes will fix a missing server header.

It removes the error for you during development, but your users' browsers will still enforce CORS normally. Disabling CORS in your browser is a temporary workaround, not a fix. The real solution is configuring the server with proper CORS headers.

A preflight request is an automatic OPTIONS request the browser sends before certain cross-origin requests to check whether the server will accept them. It happens for requests with custom headers, non-simple HTTP methods, or JSON content types. If the preflight fails, the actual request never gets sent.