CORS in Production: Implementation, Debugging, and Security Best Practices
Most developers first encounter CORS when they see a cryptic browser error:
CORS in Production: Implementation, Debugging, and Security Best Practices
Most developers first encounter CORS when they see a cryptic browser error:
Access to fetch at ‘https://api.example.com' from origin ‘https://frontend.com' has been blocked by CORS policy.
The common reaction is to search for a quick fix, add cors() middleware, and move on.
Unfortunately, that approach often creates security vulnerabilities or leads to configuration problems later in production.
In this article, we’ll explore how to properly implement CORS in backend applications, troubleshoot common issues, and follow security best practices that scale to enterprise systems and microservice architectures.
Implementing CORS Correctly
NestJS
NestJS provides built-in CORS support through the underlying Express or Fastify adapter.
A basic setup looks like this:
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableCors();
await app.listen(3000);
}
While convenient during development, production systems should use explicit configuration:
app.enableCors({
origin: [
'https://frontend.com',
'https://app.frontend.com'
],
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 3600
});
This configuration ensures that only trusted frontend applications can access the API.
Dynamic Origin Validation
In many SaaS applications, allowed origins may vary by tenant or environment.
app.enableCors({
origin: (origin, callback) => {
const allowedOrigins = [
'https://frontend.com',
/\.example\.com$/
];
if (
!origin ||
allowedOrigins.some(item =>
typeof item === 'string'
? item === origin
: item.test(origin)
)
) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true
});
This approach provides flexibility without sacrificing security.
Express.js Configuration
Express applications commonly rely on the cors package.
Allowing a Single Origin
const cors = require('cors');
app.use(cors({
origin: 'https://frontend.com',
credentials: true
}));
Multiple Allowed Origins
const allowedOrigins = [
'https://frontend.com',
'https://app.frontend.com'
];
app.use(cors({
origin: function(origin, callback) {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true
}));
Route-Specific Policies
Public endpoints and private endpoints often require different rules.
app.get('/public', cors(), handler);
app.get('/private', cors({
origin: 'https://frontend.com',
credentials: true
}), handler);
This allows finer control over API exposure.
Django Configuration
Using django-cors-headers:
INSTALLED_APPS = [
'corsheaders',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
]
Production configuration:
CORS_ALLOWED_ORIGINS = [
"https://frontend.com",
"https://app.frontend.com",
]
CORS_ALLOW_CREDENTIALS = True
Restricting origins is strongly preferred over:
CORS_ALLOW_ALL_ORIGINS = True
which should only be used temporarily during development.
Laravel Configuration
Laravel includes native CORS support through the framework configuration.
return [
'paths' => ['api/*'],
'allowed_methods' => ['*'],
'allowed_origins' => [
'https://frontend.com',
'https://app.frontend.com',
],
'allowed_headers' => ['*'],
'supports_credentials' => true,
];
For production workloads, it’s recommended to explicitly define methods and headers whenever possible.
Common CORS Errors and How to Fix Them
1. No Access-Control-Allow-Origin Header
Error:
No 'Access-Control-Allow-Origin' header is present on the requested resource
Cause:
The server never returns a valid CORS response.
Solution:
app.use(cors({
origin: 'https://frontend.com'
}));
Verify the response contains:
Access-Control-Allow-Origin: https://frontend.com
2. Credentials with Wildcard Origin
Incorrect configuration:
app.use(cors({
origin: '*',
credentials: true
}));
Browser error:
The value of the Access-Control-Allow-Origin header
must not be the wildcard '*' when credentials mode is include.
Correct configuration:
app.use(cors({
origin: 'https://frontend.com',
credentials: true
}));
Browsers intentionally block wildcard origins when cookies or authentication credentials are involved.
3. Preflight OPTIONS Request Returns 403
A surprisingly common issue.
Incorrect middleware order:
app.use(authMiddleware);
app.use(cors());
In this setup, authentication intercepts the OPTIONS request before CORS can process it.
Correct order:
app.use(cors());
app.use(authMiddleware);
Alternatively:
app.use((req, res, next) => {
if (req.method === 'OPTIONS') {
return res.sendStatus(200);
}
next();
});
This allows preflight requests to succeed without authentication.
4. Localhost Development Problems
Developers often assume these origins are identical:
http://localhost:3000
http://localhost:4000
They are not.
Different ports create different origins.
Backend configuration must explicitly allow the frontend:
app.use(cors({
origin: 'http://localhost:3000'
}));
5. Custom Header Rejected
Frontend:
fetch('/users', {
headers: {
'X-Request-ID': '123'
}
});
Error:
Request header field X-Request-ID is not allowed
Backend fix:
app.use(cors({
allowedHeaders: [
'Content-Type',
'Authorization',
'X-Request-ID'
]
}));
Security Best Practices
CORS misconfigurations are among the most common API security weaknesses.
Here are the practices every team should adopt.
Never Use Wildcards for Sensitive APIs
Avoid:
origin: '*'
Especially when authentication is involved.
A wildcard effectively grants access to every website on the internet.
Maintain a Strict Allowlist
Good:
const allowedOrigins = [
'https://frontend.com',
'https://app.frontend.com'
];
Bad:
origin: true
Explicit is safer than implicit.
Enable Preflight Caching
maxAge: 86400
Benefits:
- Fewer OPTIONS requests
- Reduced latency
- Lower server load
Large applications often see dramatic performance improvements from proper caching.
Restrict HTTP Methods
Prefer:
methods: [
'GET',
'POST',
'PUT',
'DELETE'
]
Instead of:
methods: '*'
Only expose functionality that is actually needed.
Enforce HTTPS
Production traffic should never rely on insecure origins.
Example:
if (
process.env.NODE_ENV === 'production' &&
origin &&
!origin.startsWith('https://')
) {
return callback(new Error('HTTPS required'));
}
This prevents accidental exposure through unsecured channels.
CORS vs Development Proxies
During development, configuring backend CORS policies may be inconvenient.
Frontend teams often use proxies.
Vite Example
export default {
server: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
rewrite: path =>
path.replace(/^\/api/, '')
}
}
}
};
Frontend code becomes:
fetch('/api/users');
The development server forwards requests behind the scenes.
Advantages
- No backend changes
- Faster local setup
- Easier frontend development
Limitations
- Development-only solution
- Not a replacement for production CORS policies
CORS in Microservice Architectures
Modern systems rarely consist of a single backend service.
A common architecture looks like this:
Frontend
│
▼
API Gateway
│
┌──┼──────────────┐
│ │ │ │
▼ ▼ ▼ ▼
Auth User Order Payment
In this setup:
- Frontend communicates with the API Gateway
- Gateway enforces CORS
- Internal services communicate privately
Only one layer needs CORS configuration.
Benefits include:
- Centralized security
- Easier maintenance
- Consistent policies across services
This pattern is widely used in large-scale systems.
Testing CORS
Never assume CORS works correctly.
Test it.
Manual Testing with cURL
Preflight test:
curl -X OPTIONS https://api.example.com/users \
-H "Origin: https://frontend.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Authorization" \
-v
Verify:
Access-Control-Allow-Origin
Access-Control-Allow-Methods
Access-Control-Allow-Headers
are present and correct.
Automated Testing
Example using Jest and Supertest:
it('allows requests from approved origins', () => {
return request(app.getHttpServer())
.get('/users')
.set('Origin', 'https://frontend.com')
.expect(200)
.expect(
'Access-Control-Allow-Origin',
'https://frontend.com'
);
});
Automated tests help prevent regressions during deployments and infrastructure changes.
Final Thoughts
CORS is often misunderstood as an annoying browser restriction, but it is actually one of the web platform’s most important security mechanisms.
A production-grade CORS strategy should include:
- Explicit origin allowlists
- Proper credential handling
- Secure preflight processing
- HTTPS enforcement
- Automated testing
- Centralized management through API gateways
When configured correctly, CORS provides a strong balance between security and interoperability, enabling modern web applications to communicate safely across origins without exposing users to cross-site attacks.
메타데이터
- post_id
- 01ea0389a0a8
- slug
- cors-in-production-implementation-debugging-and-security-best-practices-01ea0389a0a8
- url
- https://medium.com/@navidbarsalari/cors-in-production-implementation-debugging-and-security-best-practices-01ea0389a0a8
- canonical_url
- https://medium.com/@navidbarsalari/cors-in-production-implementation-debugging-and-security-best-practices-01ea0389a0a8
- author_url
- https://medium.com/@navidbarsalari
- status
- ok
- fetched_at
- 2026-06-15 20:49:13