McTaba Labs logo
By Bonaventure Ogeto|

What Is CORS? Understanding and Fixing CORS Errors

CORS (Cross-Origin Resource Sharing) is a browser security feature that blocks web pages from making requests to a different domain than the one serving the page. When your frontend at localhost:3000 tries to call an API at api.example.com, the browser blocks it unless the API server explicitly allows it. The fix is always on the server side, not in your frontend code.

Why CORS exists: the same-origin policy

Browsers enforce the same-origin policy: a page loaded from one origin (domain + port + protocol) cannot freely make requests to a different origin. Two URLs have the same origin only if the protocol, domain, and port all match.

Same origin examples:

  • https://myapp.com/page1 and https://myapp.com/page2 (same origin, different paths)

Different origin examples:

  • http://localhost:3000 and http://localhost:8000 (different ports)
  • https://myapp.com and https://api.myapp.com (different subdomains)
  • http://myapp.com and https://myapp.com (different protocols)

Why does the browser care? Imagine you are logged into your bank at bank.co.ke. You open another tab and visit a malicious site. Without the same-origin policy, that malicious site could silently make requests to bank.co.ke using your cookies and transfer your money. The same-origin policy prevents this.

CORS is the system that lets servers opt in to allowing cross-origin requests. It is not a bug or a flaw. It is a security feature doing its job.

What the CORS error looks like

You will see something like this in your browser console:

Access to fetch at 'https://api.example.com/data'
from origin 'http://localhost:3000' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.

Or this variation for requests with custom headers:

Access to fetch at 'https://api.example.com/data'
from origin 'http://localhost:3000' has been blocked by CORS policy:
Response to preflight request doesn't pass access control check:
No 'Access-Control-Allow-Origin' header is present on the requested resource.

Key things to understand:

  • The request actually reached the server. The server responded. But the browser threw away the response because the server did not include the right CORS headers.
  • This is a browser-only restriction. The same request from Postman, curl, or a server-side script works fine. That is why "it works in Postman but not in my app" is such a common complaint.
  • The fix is always on the server. You cannot disable CORS from the client side.

How CORS works: preflight requests

For simple GET requests, the browser sends the request and checks the response headers. For anything more complex (POST with JSON, requests with custom headers like Authorization), the browser sends a preflight request first.

A preflight is an OPTIONS request that asks the server: "Will you accept a POST request from this origin with these headers?"

// Preflight request (sent automatically by the browser)
OPTIONS /api/data HTTP/1.1
Host: api.example.com
Origin: http://localhost:3000
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization

The server needs to respond with headers that say "yes, I allow this":

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

Only after the preflight succeeds does the browser send the actual request.

Fixing CORS in your server

Fix in Express.js:

import cors from 'cors';

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

Fix in Next.js API routes:

// app/api/data/route.ts
export async function GET(req: Request) {
  const data = { message: 'Hello from API' };

  return Response.json(data, {
    headers: {
      'Access-Control-Allow-Origin': 'http://localhost:3000',
      'Access-Control-Allow-Methods': 'GET, POST',
      'Access-Control-Allow-Headers': 'Content-Type, Authorization',
    },
  });
}

// Handle preflight
export async function OPTIONS() {
  return new Response(null, {
    status: 204,
    headers: {
      'Access-Control-Allow-Origin': 'http://localhost:3000',
      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
      'Access-Control-Allow-Headers': 'Content-Type, Authorization',
    },
  });
}

Fix in Next.js middleware (for all API routes):

// middleware.ts
import { NextResponse } from 'next/server';

export function middleware(request: Request) {
  const response = NextResponse.next();
  response.headers.set('Access-Control-Allow-Origin', 'https://myapp.com');
  response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
  response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  return response;
}

Common CORS traps and how to avoid them

Trap 1: Using Access-Control-Allow-Origin: *

The wildcard * allows any origin. This works for public APIs but breaks when you send cookies or Authorization headers. If you need credentials: true, you must specify the exact origin, not *.

Trap 2: CORS error only in production

Your frontend and API are on the same origin in development (both on localhost) but different origins in production (app.example.com and api.example.com). You do not see CORS errors locally because there is no cross-origin request. Always test with your production domain configuration.

Trap 3: Forgetting the OPTIONS handler

Your GET route has CORS headers, but preflight OPTIONS requests return 404 because you did not add a handler for the OPTIONS method. Some frameworks handle this automatically. Others require you to add it explicitly.

Trap 4: Using a CORS proxy in production

Beginners sometimes use a CORS proxy (cors-anywhere, allorigins) to bypass CORS in development. This is fine for learning but is a security risk and performance bottleneck in production. Fix CORS properly on your own server.

The best fix: avoid CORS entirely. If your frontend and API are part of the same Next.js app, use API routes (app/api/). Requests to the same origin never trigger CORS. This is one of the advantages of full-stack frameworks.

Frequently Asked Questions

Why does my request work in Postman but fail in the browser?
CORS is a browser-only security feature. Postman, curl, and server-side code do not enforce the same-origin policy. The request reaches the server and the response comes back fine. The browser just refuses to show the response to your JavaScript code when the CORS headers are missing.
Can I disable CORS in Chrome for development?
You can launch Chrome with the --disable-web-security flag, but this disables security for all sites and is dangerous. A better approach is to fix the CORS headers on your server, use a Next.js API route as a same-origin proxy, or configure a proper development proxy in your framework.
Do I need CORS headers for server-to-server requests?
No. CORS only applies to browser-initiated requests. If your backend calls another API (like calling the Daraja API from your Node.js server), CORS does not apply. The browser never sees that request.

Ready to build real-world apps?

Join the McTaba Labs full-stack marathon. Ship 8 production apps with M-Pesa, USSD, and WhatsApp integrations, and get career support until placement.

See Programs