Complete guide to configuring CORS for your uWestJS HTTP server.
- Overview
- Quick Start
- Configuration Options
- Origin Validation
- Credentials
- Preflight Requests
- Security Best Practices
- Examples
CORS (Cross-Origin Resource Sharing) is a security feature that controls which origins can access your API. uWestJS provides flexible CORS configuration through the enableCors() method.
When do you need CORS?
- Your frontend is hosted on a different domain than your API
- You're building a public API that will be accessed from browsers
- You need to allow specific origins to make authenticated requests
import { NestFactory } from '@nestjs/core';
import { UwsPlatformAdapter } from 'uwestjs';
async function bootstrap() {
const app = await NestFactory.create(
AppModule,
new UwsPlatformAdapter()
);
// Allow all origins (development only!)
app.enableCors();
await app.listen(3000);
}
bootstrap();async function bootstrap() {
const app = await NestFactory.create(
AppModule,
new UwsPlatformAdapter()
);
// Allow specific origin
app.enableCors({
origin: 'https://example.com',
credentials: true,
});
await app.listen(3000);
}
bootstrap();interface CorsOptions {
/**
* Allowed origin(s)
* - string: Single origin (e.g., 'https://example.com')
* - string[]: Multiple origins
* - boolean: true = allow all origins, false = deny all
* - '*': All origins (equivalent to true)
* - (origin) => boolean | Promise<boolean>: Dynamic validation (sync or async)
*
* Note: The origin parameter can be null in privacy-sensitive contexts
* (sandboxed iframes, local files, data: URLs)
*
* Security Warning: Wildcard origins ('*' or true) CANNOT be combined
* with credentials: true per CORS specification
*/
origin?: string | string[] | boolean | ((origin: string | null) => boolean | Promise<boolean>);
/**
* Allow credentials (cookies, authorization headers)
* Default: false
*/
credentials?: boolean;
/**
* Allowed HTTP methods
* Default: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE']
*/
methods?: string | string[];
/**
* Headers that clients can send
* Default: ['Content-Type', 'Authorization']
*/
allowedHeaders?: string | string[];
/**
* Headers that are exposed to the client
* Default: []
*/
exposedHeaders?: string | string[];
/**
* How long preflight results can be cached (seconds)
* Default: 86400 (24 hours)
*/
maxAge?: number;
}app.enableCors({
origin: 'https://example.com',
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key'],
exposedHeaders: ['X-Total-Count', 'X-Page-Number'],
maxAge: 3600, // 1 hour
});app.enableCors({
origin: 'https://example.com',
});app.enableCors({
origin: [
'https://example.com',
'https://app.example.com',
'https://admin.example.com',
],
});// Development only!
app.enableCors({
origin: '*',
});Warning: Never use origin: '*' with credentials: true in production. This is a security vulnerability.
app.enableCors({
origin: (origin) => {
// Allow all subdomains of example.com
if (origin?.endsWith('.example.com')) {
return true;
}
// Allow specific origins (use exact matching for security)
const allowedOrigins = [
'https://example.com',
'https://partner.com',
];
return origin ? allowedOrigins.includes(origin) : false;
},
credentials: true,
});app.enableCors({
origin: (origin) => {
if (process.env.NODE_ENV === 'development') {
// Allow localhost in development (use startsWith for security)
return (origin?.startsWith('http://localhost:') || origin === 'http://localhost') ?? false;
}
// Production: strict validation with exact matching
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || [];
return origin ? allowedOrigins.includes(origin) : false;
},
credentials: true,
});Some contexts (sandboxed iframes, local files) send null as the origin:
app.enableCors({
origin: (origin) => {
// Reject null origins for security
if (!origin) {
return false;
}
// Validate non-null origins
return origin.endsWith('.example.com');
},
});app.enableCors({
origin: 'https://example.com',
credentials: true, // Allow cookies and auth headers
});fetch('https://api.example.com/data', {
method: 'GET',
credentials: 'include', // Send cookies
headers: {
'Authorization': 'Bearer token',
},
});axios.get('https://api.example.com/data', {
withCredentials: true, // Send cookies
headers: {
'Authorization': 'Bearer token',
},
});Browsers send preflight OPTIONS requests for:
- Non-simple methods (PUT, DELETE, PATCH)
- Custom headers
- Content-Type other than application/x-www-form-urlencoded, multipart/form-data, or text/plain (e.g., application/json)
uWestJS automatically handles preflight requests:
app.enableCors({
origin: 'https://example.com',
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
maxAge: 3600, // Cache preflight for 1 hour
});If you need custom preflight logic:
@Controller('api')
export class ApiController {
@Options('*')
handlePreflight(@Res() res: UwsResponse) {
// Custom preflight logic
res.status(204).send();
}
}// DANGEROUS - Security vulnerability!
app.enableCors({
origin: '*',
credentials: true,
});
// SAFE - Specific origins
app.enableCors({
origin: 'https://example.com',
credentials: true,
});// Good - strict validation
app.enableCors({
origin: (origin) => {
const allowedOrigins = [
'https://example.com',
'https://app.example.com',
];
return origin ? allowedOrigins.includes(origin) : false;
},
});
// Bad - loose validation
app.enableCors({
origin: (origin) => {
return origin?.includes('example') ?? false; // Too permissive!
},
});// Good - only needed methods
app.enableCors({
methods: ['GET', 'POST'],
});
// Bad - all methods (including unnecessary ones)
app.enableCors({
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD'],
});
// Note: OPTIONS is handled automatically for preflight and doesn't need to be listed// Good - only necessary headers
app.enableCors({
exposedHeaders: ['X-Total-Count'],
});
// Bad - exposing sensitive headers
app.enableCors({
exposedHeaders: ['X-API-Key', 'X-Internal-Token'], // Don't expose secrets!
});app.enableCors({
origin: (origin) => {
if (process.env.NODE_ENV === 'production') {
// Require HTTPS in production
return origin?.startsWith('https://') ?? false;
}
return true;
},
});// Allow all origins, no credentials
app.enableCors({
origin: '*',
methods: ['GET'],
allowedHeaders: ['Content-Type'],
});// Specific origins with credentials
app.enableCors({
origin: [
'https://example.com',
'https://app.example.com',
],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
exposedHeaders: ['X-Total-Count', 'X-Page-Number'],
maxAge: 3600,
});const corsOptions: CorsOptions = {
origin: process.env.NODE_ENV === 'production'
? ['https://example.com', 'https://app.example.com']
: ['http://localhost:3000', 'http://localhost:4200'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
};
app.enableCors(corsOptions);app.enableCors({
origin: (origin) => {
if (!origin) return false;
// Allow all tenant subdomains
const tenantPattern = /^https:\/\/[\w-]+\.example\.com$/;
if (tenantPattern.test(origin)) {
return true;
}
// Allow main domain
return origin === 'https://example.com';
},
credentials: true,
});app.enableCors({
origin: 'https://example.com',
credentials: true,
allowedHeaders: [
'Content-Type',
'Authorization',
'X-API-Key',
'X-Request-ID',
'X-Client-Version',
],
exposedHeaders: [
'X-RateLimit-Limit',
'X-RateLimit-Remaining',
'X-RateLimit-Reset',
],
});app.enableCors({
origin: (origin) => {
// Allow requests from other microservices
const internalServices = [
'http://auth-service:3001',
'http://user-service:3002',
'http://payment-service:3003',
];
if (origin && internalServices.includes(origin)) {
return true;
}
// Allow external clients
return origin === 'https://example.com';
},
credentials: true,
});app.enableCors({
origin: (origin) => {
if (!origin) {
// SECURITY WARNING: Accepting null origins is risky!
// Native mobile apps typically don't send CORS requests.
// If you need to support null origins, implement proper validation:
// - Check API key in Authorization header
// - Validate request signature
// - Use authentication guards/middleware
//
// For this example, we reject null origins for security.
// Use direct HTTP requests from mobile apps instead of browser-based requests.
return false;
}
// Web clients must be from allowed domains
return origin.endsWith('.example.com');
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
});Note: Native mobile apps (iOS, Android) should make direct HTTP requests, not browser-based requests that trigger CORS. If you must accept null origins, implement robust authentication using API keys, JWT tokens, or request signatures in Guards/Middleware, not in the CORS validation function.
Cause: Origin not allowed
Solution:
// Check your origin configuration
app.enableCors({
origin: 'https://your-frontend-domain.com', // Must match exactly
});Cause: Credentials not enabled on server
Solution:
app.enableCors({
origin: 'https://example.com',
credentials: true, // Add this
});Cause: HTTP method not in allowed methods
Solution:
app.enableCors({
methods: ['GET', 'POST', 'PUT', 'DELETE'], // Add your method
});Cause: Custom header not in allowed headers
Solution:
app.enableCors({
allowedHeaders: ['Content-Type', 'Authorization', 'X-Custom-Header'],
});Cause: OPTIONS request not handled
Solution: uWestJS handles OPTIONS automatically for CORS preflight. Ensure CORS is enabled before routes:
// Ensure CORS is enabled before routes
app.enableCors({
origin: 'https://example.com',
methods: ['GET', 'POST', 'PUT', 'DELETE'],
});
// Note: 'OPTIONS' does not need to be explicitly listed;
// it is handled automatically for preflight requests.# Test simple request
curl -H "Origin: https://example.com" \
-H "Access-Control-Request-Method: GET" \
-H "Access-Control-Request-Headers: Content-Type" \
-X OPTIONS \
http://localhost:3000/api/data
# Test with credentials
curl -H "Origin: https://example.com" \
-H "Cookie: session=abc123" \
http://localhost:3000/api/data// Test CORS from browser console
fetch('http://localhost:3000/api/data', {
method: 'GET',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
})
.then(response => response.json())
.then(data => console.log('Success:', data))
.catch(error => console.error('CORS Error:', error));- Server - Server configuration
- Security Best Practices - MDN CORS Guide
- OWASP CORS - Security guidelines