PureTools

CORS Explained: Why Your API Call Is Blocked

PureTools Team· 7 min read
CORS Explained: Why Your API Call Is Blocked

CORS: The Error Every Developer Has Seen

You're building a frontend that calls your API. It works in Postman. It works with curl. But the browser says:

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.

This isn't a server error. It's the browser protecting the user.

What CORS Actually Is

CORS (Cross-Origin Resource Sharing) is a browser security mechanism. By default, JavaScript running on origin A cannot make requests to origin B. An origin is the combination of protocol + domain + port:

URLOrigin
http://localhost:3000http://localhost:3000
https://myapp.comhttps://myapp.com
https://api.myapp.comhttps://api.myapp.com (different subdomain = different origin)
https://myapp.com:8080https://myapp.com:8080 (different port = different origin)

How It Works

For simple requests (GET, POST with standard content types), the browser sends the request normally but checks the response headers. If the response doesn't include Access-Control-Allow-Origin, the browser blocks JavaScript from reading the response.

For complex requests (PUT, DELETE, custom headers, JSON content type), the browser sends a preflight request first:

OPTIONS /api/data HTTP/1.1
Origin: http://localhost:3000
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization

The server must respond with:

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

Fixing CORS

Express.js:

import cors from 'cors';

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

// Production: specify allowed origins
app.use(cors({
  origin: ['https://myapp.com', 'https://staging.myapp.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true, // if using cookies
}));

Next.js API routes:

export async function GET(request: Request) {
  const data = await getData();
  return Response.json(data, {
    headers: {
      'Access-Control-Allow-Origin': 'https://myapp.com',
      'Access-Control-Allow-Methods': 'GET, POST',
    },
  });
}

Nginx:

location /api/ {
    add_header 'Access-Control-Allow-Origin' 'https://myapp.com';
    add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
    add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';

    if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Max-Age' 86400;
        return 204;
    }
}

Common Mistakes

  • Using * with credentials. Access-Control-Allow-Origin: * doesn't work with credentials: true. You must specify the exact origin.
  • Forgetting the OPTIONS handler. Your server must respond to OPTIONS preflight requests with CORS headers and a 204 status.
  • Fixing it on the frontend. CORS is a server-side configuration. No amount of frontend code can bypass it (that's the point).
  • Proxying in dev, forgetting in prod. next.config.js rewrites work in development but not in production. You need actual CORS headers on the API server.

Alternatives to CORS

  • Same-origin API proxy: Route API calls through your own server (/api/proxy) so the browser sees same-origin requests.
  • Server-side rendering: Fetch data on the server (Next.js Server Components), not in the browser. No CORS needed.

Debug your headers: HTTP Reference — check status codes and headers for your API responses.