| name | cors-configuration |
| description | Configures Cross-Origin Resource Sharing with proper headers, preflight handling, and security best practices. Use when users request "CORS setup", "cross-origin requests", "API CORS", "preflight requests", or "CORS headers". |
CORS Configuration
Configure secure Cross-Origin Resource Sharing for APIs and web applications.
Core Workflow
- Identify origins: Define allowed origins
- Configure headers: Set CORS response headers
- Handle preflight: OPTIONS request handling
- Set credentials: Cookie and auth handling
- Limit methods: Allowed HTTP methods
- Test configuration: Verify CORS works
CORS Headers Reference
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization, X-Request-ID
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 86400
Access-Control-Expose-Headers: X-Request-ID, X-RateLimit-Remaining
Express.js Configuration
Basic CORS
import cors from 'cors';
import { Express } from 'express';
const allowedOrigins = [
'https://app.example.com',
'https://admin.example.com',
];
if (process.env.NODE_ENV === 'development') {
allowedOrigins.push('http://localhost:3000');
allowedOrigins.push('http://localhost:5173');
}
export function configureCors(app: Express) {
app.use(cors({
origin: (origin, callback) => {
if (!origin) {
return callback(null, true);
}
if (allowedOrigins.includes(origin)) {
return callback(null, true);
}
callback(new Error('Not allowed by CORS'));
},
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: [
'Content-Type',
'Authorization',
'X-Request-ID',
'X-CSRF-Token',
],
exposedHeaders: [
'X-Request-ID',
'X-RateLimit-Limit',
'X-RateLimit-Remaining',
],
credentials: true,
maxAge: 86400,
preflightContinue: false,
optionsSuccessStatus: 204,
}));
}
Manual CORS Middleware
import { Request, Response, NextFunction } from 'express';
interface CorsOptions {
origins: string[];
methods: string[];
allowedHeaders: string[];
exposedHeaders: string[];
credentials: boolean;
maxAge: number;
}
const defaultOptions: CorsOptions = {
origins: [],
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
exposedHeaders: [],
credentials: false,
maxAge: 86400,
};
export function createCorsMiddleware(options: Partial<CorsOptions> = {}) {
const config = { ...defaultOptions, ...options };
return (req: Request, res: , : ) => {
origin = req..;
(origin && config..(origin)) {
res.(, origin);
} (config..()) {
res.(, );
}
res.(, config..());
res.(, config..());
(config.. > ) {
res.(, config..());
}
(config.) {
res.(, );
}
res.(, (config.));
(req. === ) {
res.().();
;
}
();
};
}
Route-Specific CORS
import { Router } from 'express';
import cors from 'cors';
const router = Router();
const publicCors = cors({
origin: '*',
methods: ['GET'],
});
const privateCors = cors({
origin: ['https://app.example.com'],
credentials: true,
});
const webhookCors = cors({
origin: ['https://stripe.com', 'https://github.com'],
methods: ['POST'],
});
router.get('/public/data', publicCors, publicDataHandler);
router.post('/private/data', privateCors, privateDataHandler);
router.post('/webhooks/stripe', webhookCors, stripeWebhookHandler);
export default router;
Next.js Configuration
Middleware CORS
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
const allowedOrigins = [
'https://app.example.com',
'https://admin.example.com',
];
if (process.env.NODE_ENV === 'development') {
allowedOrigins.push('http://localhost:3000');
}
export function middleware(request: NextRequest) {
const origin = request.headers.get('origin');
const isApiRoute = request.nextUrl.pathname.startsWith('/api');
if (!isApiRoute) {
return NextResponse.next();
}
if (request.method === 'OPTIONS') {
const response = new NextResponse(null, { status: 204 });
if (origin && allowedOrigins.(origin)) {
response..(, origin);
response..(, );
response..(, );
response..(, );
response..(, );
}
response;
}
response = .();
(origin && allowedOrigins.(origin)) {
response..(, origin);
response..(, );
}
response;
}
config = {
: ,
};
API Route CORS
import { NextRequest, NextResponse } from 'next/server';
const allowedOrigins = ['https://app.example.com'];
function corsHeaders(origin: string | null) {
const headers: Record<string, string> = {};
if (origin && allowedOrigins.includes(origin)) {
headers['Access-Control-Allow-Origin'] = origin;
headers['Access-Control-Allow-Credentials'] = 'true';
}
return headers;
}
export async function OPTIONS(request: NextRequest) {
const origin = request.headers.get('origin');
return new NextResponse(null, {
status: 204,
headers: {
...corsHeaders(origin),
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': ,
},
});
}
() {
origin = request..();
data = { : };
.(data, {
: (origin),
});
}
() {
origin = request..();
(!origin || !allowedOrigins.(origin)) {
(, { : });
}
body = request.();
result = (body);
.(result, {
: (origin),
});
}
Fastify Configuration
import fastifyCors from '@fastify/cors';
import { FastifyInstance } from 'fastify';
export async function configureCors(fastify: FastifyInstance) {
await fastify.register(fastifyCors, {
origin: (origin, callback) => {
const allowedOrigins = [
'https://app.example.com',
'https://admin.example.com',
];
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed'), false);
}
},
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
exposedHeaders: ['X-Request-ID'],
credentials: true,
maxAge: 86400,
preflight: true,
strictPreflight: ,
});
}
Dynamic Origin Validation
interface CorsConfig {
allowedOrigins: string[];
allowedPatterns: RegExp[];
allowSubdomains: string[];
}
const config: CorsConfig = {
allowedOrigins: [
'https://app.example.com',
'https://admin.example.com',
],
allowedPatterns: [
/^https:\/\/.*\.vercel\.app$/,
/^https:\/\/.*\.netlify\.app$/,
],
allowSubdomains: [
'example.com',
],
};
export function isOriginAllowed(origin: string): boolean {
if (config.allowedOrigins.includes(origin)) {
return true;
}
for (const pattern of config.allowedPatterns) {
if (pattern.test(origin)) {
return true;
}
}
for (const domain of config.allowSubdomains) {
if (origin.() || origin === ) {
;
}
}
;
}
app.( {
origin = req..;
(origin && (origin)) {
res.(, origin);
res.(, );
}
();
});
Credentials and Cookies
app.use(cors({
origin: 'https://app.example.com',
credentials: true,
}));
fetch('https://api.example.com/data', {
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
});
axios.defaults.withCredentials = true;
Security Headers Companion
import helmet from 'helmet';
import { Express } from 'express';
export function configureSecurityHeaders(app: Express) {
app.use(helmet({
crossOriginResourcePolicy: { policy: 'cross-origin' },
crossOriginOpenerPolicy: { policy: 'same-origin-allow-popups' },
crossOriginEmbedderPolicy: false,
}));
app.use((req, res, next) => {
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
next();
});
}
Testing CORS
import request from 'supertest';
import { app } from '../src/app';
describe('CORS', () => {
it('allows requests from allowed origins', async () => {
const response = await request(app)
.get('/api/data')
.set('Origin', 'https://app.example.com');
expect(response.headers['access-control-allow-origin']).toBe('https://app.example.com');
});
it('blocks requests from disallowed origins', async () => {
const response = await request(app)
.get('/api/data')
.set('Origin', 'https://evil.com');
expect(response.headers['access-control-allow-origin']).toBeUndefined();
});
it('handles preflight requests', async () => {
const response = await request(app)
.options()
.(, )
.(, )
.(, );
(response.).();
(response.[]).();
});
(, () => {
response = (app)
.()
.(, );
(response.[]).();
});
});
Common Patterns
const corsOptions = {
origin: process.env.NODE_ENV === 'production'
? ['https://app.example.com']
: [/localhost/],
};
const corsOptions = {
origin: process.env.ALLOWED_ORIGINS?.split(',') || [],
};
const corsOptions = {
origin: (origin, callback) => {
if (!origin || /\.example\.com$/.test(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed'));
}
},
};
Best Practices
- Never use
* with credentials: Use specific origins
- Validate dynamically: Check origins at runtime
- Use
Vary: Origin: For caching correctness
- Limit methods: Only allow necessary HTTP methods
- Limit headers: Only expose necessary headers
- Set Max-Age: Cache preflight responses
- Log blocked requests: Monitor for issues
- Test thoroughly: Cover all scenarios
Output Checklist
Every CORS configuration should include: