If you’ve ever built a React app and tried calling an external API, chances are you’ve run into the dreaded CORS error:
Access to fetch at 'https://api.example.com/data' from origin 'http://localhost:5173'
has been blocked by CORS policy
Sound familiar? Don’t worry—you’re not alone. In this article, I’ll walk you through why this happens and how to fix it properly using a Node/Express proxy middleware. This method works in both development and production and is the most reliable way to deal with CORS.
What is CORS? (Quick Recap)
- CORS = Cross-Origin Resource Sharing.
- Browsers block requests from one domain (e.g.,
http://localhost:5173
) to another (e.g.,https://api.example.com
) unless the server explicitly allows it. - You cannot fix CORS errors from the frontend. Only the server can allow cross-origin requests.
👉 The solution: Use a proxy server.
How Does a Proxy Fix CORS?
React App (http://localhost:5173) → /proxy → Express Middleware → https://dummyapi.com
- React calls
/proxy
→ same origin → no CORS. - Express receives the request, forwards it to the real API (
https://dummyapi.com
). - Express sends the API response back to React.
- Browser is happy. 🎉
Step 1 — Express Proxy Middleware
Install Express:
npm install express
npm install -D typescript ts-node @types/express nodemon
server.ts
import express, { Request, Response, NextFunction } from 'express';
const app = express();
app.use('/proxy', async (req: Request, res: Response, next: NextFunction) => {
try {
const targetBase = 'https://dummyapi.com'; // Dummy external API
const proxiedPath = req.originalUrl.replace(/^\/proxy/, '') || '/';
const targetUrl = `${targetBase}${proxiedPath}`;
const outgoingHeaders: Record<string, string> = {};
Object.entries(req.headers).forEach(([k, v]) => {
const key = k.toLowerCase();
if (['host', 'connection', 'keep-alive', 'transfer-encoding', 'content-encoding'].includes(key)) return;
if (typeof v === 'string') outgoingHeaders[key] = v;
else if (Array.isArray(v)) outgoingHeaders[key] = v.join(',');
});
const fetchOptions: any = {
method: req.method,
headers: outgoingHeaders,
redirect: 'manual',
body: ['GET', 'HEAD'].includes(req.method) ? undefined : (req as any),
};
const upstreamRes = await fetch(targetUrl, fetchOptions);
res.status(upstreamRes.status);
upstreamRes.headers.forEach((value, name) => {
if (!['transfer-encoding', 'connection', 'keep-alive', 'content-encoding'].includes(name.toLowerCase())) {
res.setHeader(name, value as string);
}
});
const body = upstreamRes.body;
if (body && typeof (body as any).pipe === 'function') {
(body as any).pipe(res);
} else {
const text = await upstreamRes.text();
res.send(text);
}
} catch (err) {
next(err);
}
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Proxy server running on http://localhost:${PORT}`);
});
Step 2 — Configure Vite Proxy (Dev Mode)
vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/proxy': {
target: 'http://localhost:3000',
changeOrigin: true,
secure: false,
},
},
},
});
Step 3 — Use .env
in React
.env
VITE_API_BASE_URL=/proxy
API client:
import axios from 'axios';
export const api = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
});
Step 4 — Call API in React
import { useEffect, useState } from 'react';
import { api } from './lib/api';
function App() {
const [data, setData] = useState<any[]>([]);
useEffect(() => {
api.get('/posts') // Actually calls http://localhost:3000/proxy/posts
.then((res) => setData(res.data))
.catch(console.error);
}, []);
return (
<div>
<h1>Posts from Dummy API</h1>
<ul>
{data.map((item) => (
<li key={item.id}>{item.title}</li>
))}
</ul>
</div>
);
}
export default App;
Step 5 — Run Locally
Run backend:
npx ts-node server.ts
Run frontend:
npm run dev
Visit:
- React: http://localhost:5173
- Proxy: http://localhost:3000
✅ No CORS errors.
Step 6 — Production Deployment Guide 🚀
Here are three easy options:
🔹 Option A: Serve React from Express (Single Origin)
- Build React app:
npm run build
This generates a dist/
folder.
- Update Express to serve static files:
import path from 'path';
app.use(express.static(path.join(__dirname, '../dist')));
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, '../dist/index.html'));
});
- Deploy this single Express app to Render, Railway, or your own VPS.
Everything runs on one origin (e.g.,
https://myapp.onrender.com
).
✅ No extra configuration required.
🔹 Option B: React on Netlify/Vercel + Express on Render
- Deploy React frontend on Netlify/Vercel.
- Deploy your Express proxy server on Render (free tier available).
- Configure Netlify/Vercel rewrites:
netlify.toml
[[redirects]]
from = "/proxy/*"
to = "https://your-express-service.onrender.com/proxy/:splat"
status = 200
✅ Now React /proxy
calls will hit your Express proxy on Render.
🔹 Option C: Reverse Proxy with Nginx
If hosting yourself (VPS, AWS, DigitalOcean):
server {
server_name myapp.com;
location / {
root /var/www/myapp/dist;
index index.html;
try_files $uri /index.html;
}
location /proxy/ {
proxy_pass http://localhost:3000/proxy/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
✅ /proxy
is transparently forwarded to Express.
✅ Frontend served by Nginx.
Step 7 — Security Best Practices
- Lock proxy target (no open proxies).
- Timeouts & retries (avoid hanging requests).
- Rate limiting (protect from abuse).
- API keys stay on server (never expose in frontend).
- HTTPS everywhere (secure traffic).
Final Thoughts
CORS errors frustrate almost every React developer at some point. Instead of disabling security or using hacks, the proxy middleware pattern is the most robust, production-ready solution.
- Works locally & in production.
- Keeps frontend code clean.
- Adds a security layer (hide API keys).
Whether you deploy on Render, Netlify/Vercel, or your own server, this setup ensures your React app works seamlessly without CORS headaches.
Top comments (1)
A solid approach! Good that you went beyond the usual dev only proxy setup and showed a production friendly Express Proxy with header handling. Nice