LearnFrenzy
Sign in Take a free test

REST API Security · Lesson 17 of 30

CORS Explained for REST APIs

4 min read Updated Nov 8, 2026 Saurabh Samir

"Blocked by CORS policy" is the error every frontend developer meets sooner or later. CORS (Cross-Origin Resource Sharing) isn't a bug or an API security feature – it's the browser relaxing its same-origin policy when your API explicitly says a website may read its responses. Once you know which headers the browser checks, CORS errors take minutes to fix.

In this lesson

  • Origins and the same-origin policy
  • Simple requests vs preflight
  • Configuring CORS in Express – safely
  • Credentials and common mistakes

Origins

An origin is scheme + host + port: https://app.learnfrenzy.com and https://api.learnfrenzy.com are different origins, and so are http://localhost:5173 and http://localhost:3000. By default, JavaScript on one origin may send requests to another, but the browser won't let it read the response unless the server opts in with CORS headers.

CORS only applies in browsers

curl, Postman, servers and mobile apps ignore CORS completely. That's why "it works in Postman but not in the browser" is the classic symptom – and why CORS is not a way to protect your API. Authentication and authorisation do that.

Simple requests and preflight

A preflighted request

  1. Preflightthe browser sends OPTIONS with Origin, Access-Control-Request-Method and -Headers
  2. Permissionthe API answers with Access-Control-Allow-Origin, -Methods, -Headers (and Max-Age)
  3. Real requestonly if allowed: the PUT/DELETE/JSON POST is sent
  4. Readthe response also needs Access-Control-Allow-Origin, or JavaScript can't read it

GET, HEAD and form-style POSTs with simple headers skip the preflight. Anything else – PUT, DELETE, Content-Type: application/json, an Authorization header – triggers one.

CORS in Express

// server.js
import express from 'express';
import cors from 'cors';

const app = express();
app.use(express.json());

const ALLOWED = ['https://app.learnfrenzy.com', 'http://localhost:5173'];

app.use(cors({
  origin: (origin, done) => done(null, !origin || ALLOWED.includes(origin)),   // allow-list, not '*'
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,                                                           // allow cookies / auth
  maxAge: 600,                                                                 // cache preflight for 10 minutes
}));

app.put('/api/profile', (req, res) => res.json({ saved: true, name: req.body.name }));

app.listen(process.env.PORT ?? 3000);

Real responses (what the browser would see)

$ curl -X OPTIONS http://localhost:3000/api/profile -H 'Origin: https://app.learnfrenzy.com' -H 'Access-Control-Request-Method: PUT' -H 'Access-Control-Request-Headers: content-type,authorization'
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.learnfrenzy.com
Vary: Origin
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET,POST,PUT,DELETE
Access-Control-Allow-Headers: Content-Type,Authorization
Access-Control-Max-Age: 600

$ curl -X PUT http://localhost:3000/api/profile -H 'Origin: https://app.learnfrenzy.com' -H 'Content-Type: application/json' -d '{"name":"Asha"}'
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.learnfrenzy.com
Vary: Origin
Access-Control-Allow-Credentials: true
Content-Type: application/json; charset=utf-8

{
  "saved": true,
  "name": "Asha"
}

$ curl -X OPTIONS http://localhost:3000/api/profile -H 'Origin: https://evil.example' -H 'Access-Control-Request-Method: PUT'
HTTP/1.1 200 OK
Allow: PUT
Content-Type: text/plain

PUT
  • The allowed origin's preflight got 204 with the allowed methods and headers; the real request echoed the exact origin.
  • The unknown origin's preflight got Express's default OPTIONS reply but no Access-Control-Allow-Origin header, so the browser refuses to send the real PUT at all.
  • Careful: for simple requests (a plain GET or form POST) there is no preflight – the server runs the request and the browser only hides the response from the page. CORS limits what pages can read; it never replaces authentication and CSRF protection.
  • Vary: Origin tells caches that the response depends on the origin.

Common mistakes

MistakeFix
Access-Control-Allow-Origin: * with credentialsbrowsers reject it – echo specific allowed origins
Reflecting any Origin header backcheck against an allow-list
No OPTIONS handling (auth middleware rejects the preflight)run CORS middleware before authentication
Forgetting a custom header in allowedHeaderslist every header your frontend sends
Fixing CORS in the frontendCORS is configured on the server (or avoided with a same-origin proxy)

Practice

Try it yourself

  1. A React app on http://localhost:5173 gets a CORS error calling DELETE /api/tasks/1 with an Authorization header. What do you check?

    Show answer

    That the API answers the OPTIONS preflight (CORS middleware before auth), that http://localhost:5173 is in the allowed origins, that DELETE is in the allowed methods and Authorization in the allowed headers – and that the DELETE response itself includes Access-Control-Allow-Origin. In development, a Vite proxy can also avoid cross-origin calls entirely.

Quick check

Does CORS protect your API from curl?

No – only browsers enforce CORS.

What triggers a preflight?

Non-simple methods (PUT, DELETE…), JSON Content-Type, or custom headers like Authorization.

Can you use * with credentials?

No – return a specific origin.

Summary

  • CORS lets a server allow specific browser origins to read its responses.
  • Non-simple requests send an OPTIONS preflight first; answer it before authentication.
  • Use an origin allow-list, list methods and headers, and never combine * with credentials.

Finished this lesson?

Mark it complete to track your progress, then carry on with the next lesson.

Next lesson

Popular