Understanding CORS in Express
Imagine you're building a bridge between two cities. The bridge needs security checkpoints to control who can cross and what they can carry. In web development, CORS is that bridge, and Express helps us build and manage it. Today, we'll learn how to construct this bridge securely and efficiently, ensuring only authorized traffic can pass between our applications.
Setting Up Your First CORS Bridge
Installing the Foundation
Just as you need materials to build a bridge, you'll need the cors package to implement CORS in Express. Let's start with the installation:
npm install cors
This command adds the CORS middleware to your project, much like bringing in the steel and concrete needed for our bridge's foundation.
Building the Basic Structure
Here's how to implement the simplest version of CORS in your Express application:
// First, import our building materials
const express = require('express');
const cors = require('cors');
// Create our application foundation
const app = express();
// Install our security checkpoint
app.use(cors());
// Start listening for traffic
app.listen(3000, () => {
console.log('Our bridge is open for crossing!');
});
This basic implementation is like building a bridge that anyone can cross. While it works, it's not the most secure approach for a production environment.
Configuring Your Security Checkpoints
Understanding Each Security Measure
Origin Control
The origin option is like creating a list of authorized vehicles that can cross your bridge. You can specify exact domains or use patterns:
// Allow specific domains
origin: ['https://app.company.com', 'https://admin.company.com']
// Allow any subdomain using a regular expression
origin: [/\.company\.com$/]
// Allow dynamic origins based on conditions
origin: (origin, callback) => {
if (origin.endsWith('.company.com')) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
}
Methods Control
Like specifying which lanes vehicles can use on your bridge:
methods: ['GET', 'POST'] // Only allow reading and creating
Headers Control
Think of this as controlling what type of cargo can cross your bridge:
allowedHeaders: ['Content-Type', 'Authorization', 'X-Custom-Header']
Advanced CORS Patterns
Route-Specific CORS Rules
Sometimes you want different rules for different parts of your bridge. Here's how to implement route-specific CORS:
// Public route - anyone can access
app.get('/public', cors(), (req, res) => {
res.json({ message: 'This is public info' });
});
// Restricted route - only specific origins
const restrictedCors = cors({
origin: 'https://admin.company.com',
methods: ['POST']
});
app.post('/admin', restrictedCors, (req, res) => {
res.json({ message: 'Admin only endpoint' });
});
Dynamic CORS Configuration
For more complex scenarios, you might need to change your bridge's rules based on traffic conditions:
const dynamicCors = cors((req, callback) => {
// Read configuration from database or environment
const corsOptions = {
origin: process.env.ALLOWED_ORIGINS.split(','),
methods: req.path.startsWith('/admin')
? ['GET', 'POST']
: ['GET']
};
callback(null, corsOptions);
});
app.use(dynamicCors);
Handling CORS Issues
Troubleshooting Your Bridge
When things go wrong, here's how to diagnose and fix common CORS issues:
Missing Headers
// Problem: No Access-Control-Allow-Origin header
// Solution: Ensure CORS is properly configured
app.use(cors({
origin: true, // Enable detailed error messages
credentials: true
}));
Preflight Issues
// Handle OPTIONS requests explicitly if needed
app.options('*', cors()); // Enable pre-flight for all routes
Best Practices for Production
Keeping Your Bridge Secure
When deploying to production, follow these security guidelines:
Environment-Based Configuration
const corsOptions = {
origin: process.env.NODE_ENV === 'production'
? ['https://app.company.com']
: ['http://localhost:3000'],
credentials: true
};
app.use(cors(corsOptions));
Logging and Monitoring
app.use(cors({
origin: (origin, callback) => {
// Log CORS requests
console.log(`Request from origin: ${origin}`);
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
console.warn(`Rejected CORS request from: ${origin}`);
callback(new Error('Not allowed by CORS'));
}
}
}));
Testing Your CORS Configuration
Verifying Your Bridge is Working
Here's how to test your CORS implementation:
// Test file: cors.test.js
const request = require('supertest');
const app = require('./app');
describe('CORS Configuration', () => {
test('should allow requests from allowed origin', async () => {
const response = await request(app)
.get('/api/test')
.set('Origin', 'https://trusted-site.com');
expect(response.headers['access-control-allow-origin'])
.toBe('https://trusted-site.com');
});
test('should reject requests from disallowed origin', async () => {
const response = await request(app)
.get('/api/test')
.set('Origin', 'https://malicious-site.com');
expect(response.headers['access-control-allow-origin'])
.toBeUndefined();
});
});
Real-World Implementation Examples
Common Scenarios
Public API Server
const express = require('express');
const cors = require('cors');
const rateLimit = require('express-rate-limit');
const app = express();
// Combine CORS with rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
});
app.use(cors({
origin: '*', // Allow all origins for public API
methods: ['GET'], // Read-only access
maxAge: 86400, // Cache preflight for 24 hours
}));
app.use(limiter);
Microservices Architecture
const express = require('express');
const cors = require('cors');
const app = express();
// Allow only internal services
const allowedServices = [
'https://auth.internal.com',
'https://api.internal.com',
'https://web.internal.com'
];
app.use(cors({
origin: (origin, callback) => {
if (!origin || allowedServices.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Service-Token']
}));