
Bypassing CORS with Proxy
How to solve CORS errors during development using proxy configuration and important considerations.

How to solve CORS errors during development using proxy configuration and important considerations.
Why did Facebook ditch REST API? The charm of picking only what you want with GraphQL, and its fatal flaws (Caching, N+1 Problem).

When you don't want to go yourself, the proxy goes for you. Hide your identity with Forward Proxy, protect your server with Reverse Proxy. Same middleman, different loyalties.

Backend: 'Done.' Frontend: 'How to use?'. Automate this conversation with Swagger.

Panic over Red Error message? Browser isn't bullying you; it's protecting you.

While developing the frontend and calling an external API, I suddenly got a CORS error:
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.
The API definitely worked, but the browser blocked it. Requests via Postman or curl worked fine. I didn't understand why.
Searching showed many "add CORS headers on the server" answers, but the problem was I couldn't modify the server. It was either an external API or the backend team was too busy for immediate changes.
So I wondered "is there a way to bypass CORS at least in development?" and discovered proxy as a solution.
I roughly knew what CORS was. "Browser blocks requests to different domains" was my understanding. But confusing parts:
Especially "doesn't using proxy create security issues?" CORS is for security, so is bypassing it okay?
The turning point was accepting "CORS is a browser policy, not a server policy."
I understood CORS through the "apartment security guard" analogy:
sequenceDiagram
participant Browser
participant Proxy as Proxy Server
participant API as External API
Note over Browser,API: CORS Error Scenario
Browser->>API: GET https://api.example.com/data
API->>Browser: 200 OK (no CORS headers)
Browser->>Browser: CORS check fails
Browser->>Browser: Error!
Note over Browser,API: Using Proxy Scenario
Browser->>Proxy: GET /api/data (same domain)
Proxy->>API: GET https://api.example.com/data
API->>Proxy: 200 OK
Proxy->>Browser: 200 OK (no CORS check)
Note right of Browser: Same domain, no CORS check!
The key is CORS is only checked by browsers. Server-to-server communication has no CORS. So with a proxy server in between:
Simplest. Add to vite.config.ts:
// vite.config.ts
import { defineConfig } from 'vite';
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
});
Usage:
// ❌ CORS error
fetch('https://api.example.com/users');
// ✅ Using proxy
fetch('/api/users'); // → https://api.example.com/users
Options:
target: Actual API server addresschangeOrigin: Change Host header to target (required!)rewrite: Path transformation (/api/users → /users)Add to package.json (simple case):
{
"proxy": "https://api.example.com"
}
Or src/setupProxy.js (complex case):
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function(app) {
app.use(
'/api',
createProxyMiddleware({
target: 'https://api.example.com',
changeOrigin: true,
pathRewrite: {
'^/api': '', // remove /api
},
})
);
};
Note: setupProxy.js applies automatically without restart.
next.config.js:
module.exports = {
async rewrites() {
return [
{
source: '/api/:path*',
destination: 'https://api.example.com/:path*',
},
];
},
};
Or use API Routes (recommended):
// pages/api/users.ts
export default async function handler(req, res) {
const response = await fetch('https://api.example.com/users');
const data = await response.json();
res.status(200).json(data);
}
Usage:
// Client
fetch('/api/users'); // Call Next.js API Route
// webpack.config.js
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
pathRewrite: { '^/api': '' },
},
},
},
};
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express();
app.use(
'/api',
createProxyMiddleware({
target: 'https://api.example.com',
changeOrigin: true,
pathRewrite: { '^/api': '' },
})
);
app.listen(3000);
// vite.config.ts
export default defineConfig({
server: {
proxy: {
'/api/v1': {
target: 'https://api1.example.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/v1/, ''),
},
'/api/v2': {
target: 'https://api2.example.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/v2/, ''),
},
},
},
});
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
configure: (proxy, options) => {
proxy.on('proxyReq', (proxyReq, req, res) => {
// Add API key to all requests
proxyReq.setHeader('Authorization', 'Bearer YOUR_API_KEY');
});
},
},
},
},
});
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
secure: false, // Allow self-signed certificates
},
},
},
});
export default defineConfig({
server: {
proxy: {
'/socket': {
target: 'ws://localhost:5000',
ws: true, // Enable WebSocket
},
},
},
});
export default defineConfig({
server: {
proxy: {
'/api': {
target: process.env.VITE_API_URL || 'https://api.example.com',
changeOrigin: true,
},
},
},
});
.env.local:
VITE_API_URL=https://dev-api.example.com
Proxy configuration only works on dev server. Doesn't work after build.
// ❌ Doesn't work in production
fetch('/api/users'); // 404 error!
Solution 1: Use environment variables
const API_URL = import.meta.env.VITE_API_URL || '/api';
// Development: /api/users (proxy)
// Production: https://api.example.com/users
fetch(`${API_URL}/users`);
# .env.development
VITE_API_URL=/api
# .env.production
VITE_API_URL=https://api.example.com
Solution 2: Configure proxy on server
Nginx:
location /api {
proxy_pass https://api.example.com;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
Vercel:
// vercel.json
{
"rewrites": [
{
"source": "/api/:path*",
"destination": "https://api.example.com/:path*"
}
]
}
// ❌ Expose API key to client
fetch('/api/users', {
headers: {
'Authorization': 'Bearer SECRET_KEY', // Visible in browser!
},
});
// ✅ Add API key on server (proxy config)
proxy.on('proxyReq', (proxyReq) => {
proxyReq.setHeader('Authorization', `Bearer ${process.env.API_KEY}`);
});
2. Rate Limiting
Limit proxy requests too:
const rateLimit = require('express-rate-limit');
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // max 100 requests
});
app.use('/api', apiLimiter);
3. Allowed Domains Only
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
onProxyReq: (proxyReq, req, res) => {
const origin = req.headers.origin;
const allowedOrigins = ['http://localhost:3000', 'https://myapp.com'];
if (!allowedOrigins.includes(origin)) {
res.status(403).send('Forbidden');
}
},
},
}
changeOrigin: falseInitially forgot changeOrigin and proxy didn't work.
// ❌ Problem code
proxy: {
'/api': {
target: 'https://api.example.com',
// no changeOrigin!
},
}
// ✅ Correct code
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true, // Required!
},
}
Lesson: changeOrigin: true is almost always needed.
// ❌ Problem code
proxy: {
'/api': {
target: 'https://api.example.com/api', // /api duplicated!
rewrite: (path) => path.replace(/^\/api/, ''),
},
}
// Result: /api/users → https://api.example.com/api/users (wrong)
// ✅ Correct code
proxy: {
'/api': {
target: 'https://api.example.com',
rewrite: (path) => path.replace(/^\/api/, ''),
},
}
// Result: /api/users → https://api.example.com/users (correct)
Requested /api/users after build and got 404.
Lesson: In production, use full URL with environment variables or configure proxy on server.
Got errors proxying to API with self-signed certificate.
// ✅ Solution
proxy: {
'/api': {
target: 'https://self-signed.example.com',
changeOrigin: true,
secure: false, // Allow self-signed certificates
},
}
Note: Use secure: true in production.
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
configure: (proxy, options) => {
proxy.on('error', (err, req, res) => {
console.log('proxy error', err);
});
proxy.on('proxyReq', (proxyReq, req, res) => {
console.log('Sending Request:', req.method, req.url);
});
proxy.on('proxyRes', (proxyRes, req, res) => {
console.log('Received Response:', proxyRes.statusCode, req.url);
});
},
},
},
},
});
In browser DevTools → Network tab:
/api/users# Direct request without proxy
curl https://api.example.com/users
# Request through proxy
curl http://localhost:3000/api/users
CORS errors can be bypassed with proxy configuration in development, but production requires adding CORS headers on server or using server-side proxy.