CORS in React: Complete Beginner Guide
Cross-Origin Resource Sharing (CORS) is the mechanism by which one origin grants permission to another origin to access its resources. When a React app running at https://app.example.com needs to fetch data from https://api.example.com, the browser blocks the request by default (same-origin policy). CORS headers tell the browser that this is allowed. A single header—Access-Control-Allow-Origin: https://app.example.com—is enough to unlock cross-origin requests. Understanding CORS is essential for building modern React applications that talk to separate API servers.
CORS is not a security vulnerability; it is the standard, secure way to relax the same-origin policy. Misconfigured CORS (e.g., Access-Control-Allow-Origin: * without restrictions) is a vulnerability.
Simple Requests vs. Preflight Requests
Not all cross-origin requests trigger a CORS check. The browser distinguishes between simple requests and complex requests (also called preflight requests).
A simple request meets all of these criteria:
- Method is GET, POST, or HEAD.
- Headers are only
Accept,Accept-Language,Content-Language, orContent-Type(with specific MIME types:text/plain,application/x-www-form-urlencoded, ormultipart/form-data). - No custom headers.
For simple requests, the browser sends the request immediately and checks the CORS headers in the response.
A complex request violates one or more of the above. For example:
- Method is PUT, DELETE, PATCH, or OPTIONS.
- Custom header like
Authorization: Bearer <token>. Content-Type: application/json.
For complex requests, the browser sends a preflight OPTIONS request first. The server responds with CORS headers indicating what is allowed. Only if the preflight succeeds does the browser send the actual request.
| Request Type | Preflight? | When |
|---|---|---|
| GET with default headers | No | Browser sends immediately |
POST with Content-Type: application/json | Yes | Browser sends OPTIONS first |
POST with Content-Type: application/x-www-form-urlencoded | No | Browser sends immediately |
| PUT to any URL | Yes | All PUT requests require preflight |
GET with custom Authorization header | Yes | Custom headers require preflight |
This distinction matters for React developers: if your app sends JSON data, you are triggering a preflight, which means the server must handle OPTIONS requests.
The CORS Handshake
Here is a step-by-step example of a React app making a simple cross-origin GET request:
Step 1: React app sends fetch()
fetch('https://api.example.com/users', { method: 'GET' })
Step 2: Browser sends HTTP request
GET /users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Step 3: Server responds with CORS header
HTTP/1.1 200 OK
Content-Type: application/json
Access-Control-Allow-Origin: https://app.example.com
[response body]
Step 4: Browser checks CORS header
Comparison: Origin (https://app.example.com) matches Access-Control-Allow-Origin (https://app.example.com)
-> Allow JavaScript to read the response body
Step 5: JavaScript continues
.then(res => res.json()) // Succeeds
If the server omits the Access-Control-Allow-Origin header, the browser blocks step 5 and JavaScript sees a NetworkError.
Preflight Requests in Action
A preflight request happens when React sends a POST with JSON:
// React component making a complex request
import React, { useState } from 'react';
export default function CreateUser() {
const [name, setName] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
// This will trigger a preflight because Content-Type is application/json
const response = await fetch('https://api.example.com/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name }),
});
const result = await response.json();
alert('User created');
};
return (
<form onSubmit={handleSubmit}>
<input value={name} onChange={e => setName(e.target.value)} />
<button type="submit">Create</button>
</form>
);
}
Browser sends preflight OPTIONS request first:
OPTIONS /users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type
Server responds with preflight response:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type
Access-Control-Max-Age: 3600
Only then does the browser send the actual POST:
POST /users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Content-Type: application/json
[body: {"name": "..."}]
The Access-Control-Max-Age: 3600 header tells the browser to cache the preflight response for 3600 seconds, so the next POST to the same URL in the next hour does not require another preflight.
Common CORS Headers
| Header | Purpose | Example |
|---|---|---|
Access-Control-Allow-Origin | Which origins can access this resource | https://app.example.com or * |
Access-Control-Allow-Methods | Which HTTP methods are allowed | GET, POST, PUT, DELETE |
Access-Control-Allow-Headers | Which request headers are allowed | Content-Type, Authorization |
Access-Control-Allow-Credentials | Whether to include cookies/auth | true |
Access-Control-Max-Age | How long to cache preflight response | 3600 |
Access-Control-Expose-Headers | Which response headers JavaScript can read | X-Total-Count |
The server sets all of these headers; the browser enforces the policy.
Key Takeaways
- Simple requests (GET, POST with form data, no custom headers) do not trigger a preflight. The response must have
Access-Control-Allow-Origin. - Complex requests (POST with JSON, PUT, DELETE, custom headers) trigger a preflight OPTIONS request first. The server must handle OPTIONS and respond with the correct CORS headers.
- The browser sends the
Originheader automatically; the server compares it toAccess-Control-Allow-Originto decide whether to allow the request. - CORS is enforced by the browser, not the server. You cannot disable CORS in JavaScript.
- Misconfigured CORS is a security risk. Use specific origins, not
*(article 5 covers safe CORS configuration).
Frequently Asked Questions
Why does my React app get a CORS error even though the server sent the response?
The browser received the response but did not find the Access-Control-Allow-Origin header (or it did not match your app's origin). The response was discarded. Check the response headers in the Network tab of DevTools. The server must explicitly allow your origin.
Can I disable CORS in the browser?
No. CORS is enforced by the browser and cannot be disabled by JavaScript. Some development tools (like create-react-app proxy) bypass CORS during local development, but production must have proper server-side CORS headers.
What is the difference between CORS errors and other fetch errors?
A CORS error means the browser received a response from the server but blocked JavaScript from reading it (missing or mismatched CORS header). Other errors (network unreachable, DNS failed) occur before the server responds. Check the Network tab to distinguish.
Do I need CORS if my React app and API are on the same domain?
No. If both are at https://example.com (even https://example.com:3000 and https://example.com:3001 are different), you need CORS headers. But if truly the same origin, no CORS is needed. CORS is only for cross-origin scenarios.