Setting CORS at the web server level is often the cleanest approach. Your application doesn’t need to know about CORS at all — Nginx or Apache handles it before the request even reaches your app.
Here are configurations I’ve used in production that work.
Nginx#
Basic: Single Origin#
server {
listen 80;
server_name api.example.com;
location / {
add_header Access-Control-Allow-Origin "https://myapp.com";
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
add_header Access-Control-Allow-Headers "Content-Type, Authorization";
add_header Access-Control-Allow-Credentials "true";
add_header Access-Control-Max-Age "86400";
add_header Access-Control-Expose-Headers "X-Total-Count, X-Request-Id";
# Handle preflight
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin "https://myapp.com";
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
add_header Access-Control-Allow-Headers "Content-Type, Authorization";
add_header Access-Control-Allow-Credentials "true";
add_header Access-Control-Max-Age "86400";
return 204;
}
proxy_pass http://127.0.0.1:3000;
}
}Note: You need to repeat the add_header directives inside the if block because Nginx’s if directive creates a new context. Headers set outside the if don’t apply inside it. This is a well-known Nginx gotcha.
Advanced: Multiple Origins with Map#
The map directive is the cleanest way to handle multiple origins:
map $http_origin $cors_allowed_origin {
default "";
"~^https://(www\.)?myapp\.com$" $http_origin;
"~^https://admin\.myapp\.com$" $http_origin;
"~^https://staging\.myapp\.com$" $http_origin;
}
server {
listen 80;
server_name api.example.com;
location / {
# Only add CORS headers when origin matches
if ($cors_allowed_origin != "") {
add_header Access-Control-Allow-Origin $cors_allowed_origin always;
add_header Access-Control-Allow-Credentials "true" always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
add_header Access-Control-Max-Age "86400" always;
add_header Access-Control-Expose-Headers "X-Total-Count" always;
}
if ($request_method = OPTIONS) {
if ($cors_allowed_origin != "") {
add_header Access-Control-Allow-Origin $cors_allowed_origin always;
add_header Access-Control-Allow-Credentials "true" always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
add_header Access-Control-Max-Age "86400" always;
}
return 204;
}
proxy_pass http://127.0.0.1:3000;
}
}The always parameter ensures headers are added even for error responses (4xx, 5xx). Without it, CORS headers might be missing on error responses, which can confuse debugging.
Reverse Proxy with CORS Only on API#
If you’re serving both your frontend and API through Nginx:
# Frontend — no CORS needed
server {
listen 80;
server_name myapp.com;
root /var/www/myapp/dist;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}
# API — with CORS
server {
listen 80;
server_name api.example.com;
location / {
add_header Access-Control-Allow-Origin "https://myapp.com" always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
add_header Access-Control-Allow-Credentials "true" always;
add_header Access-Control-Max-Age "86400" always;
if ($request_method = OPTIONS) {
return 204;
}
proxy_pass http://127.0.0.1:3000;
}
}Apache#
Basic: Single Origin#
<VirtualHost *:80>
ServerName api.example.com
<IfModule mod_headers.c>
Header always set Access-Control-Allow-Origin "https://myapp.com"
Header always set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
Header always set Access-Control-Allow-Headers "Content-Type, Authorization"
Header always set Access-Control-Allow-Credentials "true"
Header always set Access-Control-Max-Age "86400"
</IfModule>
# Handle OPTIONS
RewriteEngine On
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule ^(.*)$ $1 [R=204,L]
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:3000/
ProxyPassReverse / http://127.0.0.1:3000/
</VirtualHost>Multiple Origins#
Apache uses SetEnvIf for conditional logic:
<VirtualHost *:80>
ServerName api.example.com
<IfModule mod_headers.c>
SetEnvIf Origin "^https://(www\.)?myapp\.com$" CORS_ALLOWED_ORIGIN=$0
SetEnvIf Origin "^https://admin\.myapp\.com$" CORS_ALLOWED_ORIGIN=$0
Header always set Access-Control-Allow-Origin "%{CORS_ALLOWED_ORIGIN}e" env=CORS_ALLOWED_ORIGIN
Header always set Access-Control-Allow-Credentials "true" env=CORS_ALLOWED_ORIGIN
Header always set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
Header always set Access-Control-Allow-Headers "Content-Type, Authorization"
Header always set Access-Control-Max-Age "86400"
</IfModule>
RewriteEngine On
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule ^(.*)$ $1 [R=204,L]
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:3000/
ProxyPassReverse / http://127.0.0.1:3000/
</VirtualHost>