CORS Complete Guide | Cross-Origin Resource Sharing in Node.js
이 글의 핵심
CORS (Cross-Origin Resource Sharing) is a security mechanism that controls which origins can access your API. Understanding CORS is essential for building secure web applications.
Introduction
CORS (Cross-Origin Resource Sharing) is a security feature implemented by browsers to control how web pages can request resources from different origins.
What is an Origin?
An origin consists of:
- Protocol:
httporhttps - Domain:
example.com - Port:
:3000
https://example.com:3000
└─┬──┘ └────┬─────┘└┬─┘
│ │ └─ Port
│ └───────── Domain
└─────────────────── Protocol
Same origin:
https://example.com/api/users
https://example.com/api/posts
Different origins (CORS applies):
http://localhost:3000 → http://localhost:8000
https://example.com → https://api.example.com
https://example.com → https://example.com:3000
1. The Problem
Without CORS, browsers block cross-origin requests:
// Frontend at http://localhost:3000
fetch('http://localhost:8000/api/users')
.then(res => res.json())
.catch(err => console.error(err));
// Error:
// Access to fetch at 'http://localhost:8000/api/users'
// from origin 'http://localhost:3000' has been blocked by CORS policy
2. Basic Setup
npm install cors
const express = require('express');
const cors = require('cors');
const app = express();
// Enable CORS for all origins (not recommended for production)
app.use(cors());
app.get('/api/users', (req, res) => {
res.json({ users: ['Alice', 'Bob'] });
});
app.listen(8000);
3. Specific Origins
// Allow single origin
app.use(cors({
origin: 'http://localhost:3000'
}));
// Allow multiple origins
const allowedOrigins = [
'http://localhost:3000',
'https://example.com',
'https://www.example.com'
];
app.use(cors({
origin: (origin, callback) => {
// Allow requests with no origin (like mobile apps, Postman)
if (!origin) return callback(null, true);
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
}
}));
4. Full Configuration
app.use(cors({
origin: 'http://localhost:3000',
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
exposedHeaders: ['X-Total-Count'],
credentials: true, // Allow cookies
maxAge: 3600, // Cache preflight response for 1 hour
}));
5. Credentials (Cookies)
// Server
app.use(cors({
origin: 'http://localhost:3000',
credentials: true, // Allow cookies
}));
app.get('/api/profile', (req, res) => {
// Can now read cookies
res.json({ user: req.user });
});
// Client
fetch('http://localhost:8000/api/profile', {
credentials: 'include', // Send cookies
})
.then(res => res.json());
6. Preflight Requests
For complex requests (PUT, DELETE, custom headers), browsers send a preflight OPTIONS request:
// Automatic preflight handling
app.use(cors());
// Manual preflight handling
app.options('/api/users', cors());
app.put('/api/users/:id', cors(), (req, res) => {
// Update user
});
7. Dynamic Origins
app.use(cors({
origin: (origin, callback) => {
// Allow all subdomains
if (!origin || origin.match(/\.example\.com$/)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
}
}));
// Or check database
app.use(cors({
origin: async (origin, callback) => {
const allowed = await db.allowedOrigins.findOne({ origin });
callback(null, !!allowed);
}
}));
8. Route-Specific CORS
// Enable CORS for specific routes
app.get('/api/public', cors(), (req, res) => {
res.json({ data: 'public' });
});
// No CORS for this route
app.get('/api/private', (req, res) => {
res.json({ data: 'private' });
});
// Different CORS config per route
app.get('/api/admin', cors({
origin: 'https://admin.example.com'
}), (req, res) => {
res.json({ data: 'admin' });
});
9. Custom Headers
app.use(cors({
origin: 'http://localhost:3000',
allowedHeaders: [
'Content-Type',
'Authorization',
'X-Requested-With',
'X-Custom-Header'
],
exposedHeaders: [
'X-Total-Count',
'X-Page-Count'
]
}));
// Client can now read exposed headers
fetch('http://localhost:8000/api/users')
.then(res => {
const totalCount = res.headers.get('X-Total-Count');
return res.json();
});
10. Environment-Based Configuration
const corsOptions = {
origin: process.env.NODE_ENV === 'production'
? 'https://example.com'
: 'http://localhost:3000',
credentials: true,
};
app.use(cors(corsOptions));
11. Common Errors and Solutions
Error 1: No ‘Access-Control-Allow-Origin’ header
// Problem: CORS not enabled
app.get('/api/users', (req, res) => {
res.json({ users: [] });
});
// Solution: Add CORS
app.use(cors());
Error 2: Credentials not supported with ’*‘
// Problem: Can't use credentials with wildcard origin
app.use(cors({
origin: '*',
credentials: true, // ❌ Error!
}));
// Solution: Specify exact origins
app.use(cors({
origin: 'http://localhost:3000',
credentials: true, // ✅ Works
}));
Error 3: Method not allowed
// Problem: Method not in allowed methods
app.use(cors({
methods: ['GET', 'POST'], // PUT not allowed
}));
app.put('/api/users/:id', (req, res) => {
// CORS error!
});
// Solution: Add method
app.use(cors({
methods: ['GET', 'POST', 'PUT', 'DELETE'],
}));
Error 4: Header not allowed
// Problem: Custom header not allowed
fetch('http://localhost:8000/api/users', {
headers: {
'X-Custom-Header': 'value' // Not allowed!
}
});
// Solution: Add to allowedHeaders
app.use(cors({
allowedHeaders: ['Content-Type', 'X-Custom-Header'],
}));
12. Production Configuration
const express = require('express');
const cors = require('cors');
const app = express();
// Production-ready CORS
const allowedOrigins = [
'https://example.com',
'https://www.example.com',
'https://app.example.com',
];
app.use(cors({
origin: (origin, callback) => {
// Allow requests with no origin (mobile apps, curl, Postman)
if (!origin) return callback(null, true);
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
console.warn(`CORS blocked: ${origin}`);
callback(new Error('Not allowed by CORS'));
}
},
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization'],
exposedHeaders: ['X-Total-Count'],
credentials: true,
maxAge: 86400, // 24 hours
optionsSuccessStatus: 200,
}));
app.listen(process.env.PORT || 8000);
13. With Authentication
const express = require('express');
const cors = require('cors');
const jwt = require('jsonwebtoken');
const app = express();
app.use(cors({
origin: 'http://localhost:3000',
credentials: true,
allowedHeaders: ['Content-Type', 'Authorization'],
}));
// Protected route
app.get('/api/profile', (req, res) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'No token' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
res.json({ user: decoded });
} catch (error) {
res.status(401).json({ error: 'Invalid token' });
}
});
// Client
fetch('http://localhost:8000/api/profile', {
headers: {
'Authorization': `Bearer ${token}`,
},
credentials: 'include',
});
14. CORS Proxy (Development)
For development, you can proxy requests:
// Next.js (next.config.js)
module.exports = {
async rewrites() {
return [
{
source: '/api/:path*',
destination: 'http://localhost:8000/api/:path*',
},
];
},
};
// Vite (vite.config.js)
export default {
server: {
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
},
},
},
};
15. Testing CORS
// Test CORS with curl
curl -H "Origin: http://localhost:3000" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type" \
-X OPTIONS \
http://localhost:8000/api/users
// Check response headers
// Access-Control-Allow-Origin: http://localhost:3000
// Access-Control-Allow-Methods: GET,HEAD,PUT,PATCH,POST,DELETE
Summary
CORS controls cross-origin access:
- Security mechanism implemented by browsers
- Origin checking prevents unauthorized access
- Credentials support for cookies/auth
- Preflight requests for complex operations
- Production config must be restrictive
Key Takeaways:
- Never use
origin: '*'in production - Enable credentials only for trusted origins
- Specify allowed methods and headers
- Test CORS in production-like environment
- Use proxies for development
Next Steps:
- Secure with Helmet
- Auth with Passport
- Build Express API
Resources: