The CORS Problem
When your React app runs on http://localhost:5173 and your Express backend runs on http://localhost:5000, the browser blocks requests between them. This is the Same-Origin Policy — a browser security mechanism that prevents malicious websites from making unauthorised requests to other origins.
An origin is the combination of protocol + domain + port:
http://localhost:5173— one originhttp://localhost:5000— different origin (different port)https://api.myapp.comvshttps://myapp.com— different origins (different subdomain)
CORS (Cross-Origin Resource Sharing) is a mechanism that uses HTTP headers to tell the browser which cross-origin requests are permitted.
---
Fixing CORS in Express
Install the cors npm package:
npm install cors
Allow all origins (development only — never in production):
import cors from 'cors';
app.use(cors());
Allow specific origin (production setup):
app.use(cors({
origin: process.env.CORS_ORIGIN, // e.g., 'https://myapp.com'
credentials: true, // Allow cookies/auth headers
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
}));
Settingorigin: '*'(all origins) combined withcredentials: trueis invalid and will cause a browser error. You must specify the exact origin when sending credentials.
---
Axios vs Fetch — Comparison
| Feature | Axios | Fetch |
|---|---|---|
| Installation | npm install axios | Built into browser, no install |
| Request syntax | axios.get(url, { params }) | fetch(url) |
| Response data | response.data (auto-parsed JSON) | response.json() (manual parse) |
| Error handling | Throws on 4xx/5xx status codes | Only throws on network errors |
| Interceptors | Built-in (auth headers, error handling) | Not supported natively |
| Request cancellation | CancelToken / AbortController | AbortController |
| Timeout support | timeout: 5000 option | Manual with AbortController |
| Base URL | axios.create({ baseURL }) | Manual string concatenation |
| Upload progress | Supported | Supported |
In a professional MERN project, Axios is preferred because of interceptors, automatic JSON parsing, and cleaner error handling. Fetch is fine for simple one-off requests.
---
API Base URL with Environment Variables
Hardcoding the API URL is bad practice because it differs between environments:
// src/config/api.ts
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5000';
export const axiosInstance = axios.create({
baseURL: API_BASE_URL,
withCredentials: true, // Send cookies with requests
timeout: 10000,
});
// .env.development
// VITE_API_BASE_URL=http://localhost:5000
// .env.production
// VITE_API_BASE_URL=https://api.myapp.com
---
Proxy Setup in Vite (Avoid CORS in Development)
Instead of enabling CORS, you can configure Vite to proxy API requests during development — the browser thinks all requests go to localhost:5173:
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': {
target: 'http://localhost:5000',
changeOrigin: true,
rewrite: (path) => path.replace(/^/api/, ''),
},
},
},
});
With this setup, axios.get('/api/users') in React is proxied to http://localhost:5000/users — no CORS issue.
---
Cookies vs LocalStorage for Auth Tokens
| Aspect | httpOnly Cookie | localStorage |
|---|---|---|
| XSS protection | ✅ JS cannot read httpOnly cookies | ❌ Any JS can access it |
| CSRF protection | Needs SameSite + CSRF token | ✅ Not sent automatically |
| Auto-sent | Yes (with same origin) | No (must attach manually) |
| Expiry | Set by server (maxAge/expires) | Manual, or until cleared |
| Server control | Server can revoke / set secure | Client-side only |
| Recommendation | ✅ Preferred for auth tokens | ❌ Avoid for sensitive tokens |
Store the access token in an httpOnly cookie (so JavaScript cannot steal it via XSS). The refresh token should also be in an httpOnly cookie for the same reason.
---
Common CORS Errors and Solutions
| Error | Cause | Solution |
|---|---|---|
No 'Access-Control-Allow-Origin' header | CORS not configured | Add cors() middleware before routes |
Credentials flag is true, but wildcard origin | origin: '*' with credentials: true | Set exact origin string |
| Preflight OPTIONS request failing | Missing OPTIONS handling | cors() handles this automatically |
| Cookie not being sent | withCredentials not set | Add withCredentials: true in Axios |
| Cookie not being received | Missing credentials: true in CORS config | Add credentials: true to cors options |
---
Example: React Frontend Calling Express Backend
// React component calling the backend
import axios from 'axios';
import { useState } from 'react';
const axiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
withCredentials: true,
});
function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
try {
const res = await axiosInstance.post('/api/auth/login', { email, password });
console.log('Logged in:', res.data.user);
} catch (err: any) {
console.error('Login failed:', err.response?.data?.message);
}
};
return (
<form onSubmit={handleLogin}>
<input value={email} onChange={e => setEmail(e.target.value)} />
<input type="password" value={password} onChange={e => setPassword(e.target.value)} />
<button type="submit">Login</button>
</form>
);
}