For most Node.js APIs behind Nginx, configure CORS once in Express and let Nginx reverse-proxy the requests without adding its own CORS headers. Install the cors middleware, allow the exact frontend origin, enable credentials only when required, make sure preflight OPTIONS requests reach that middleware, then validate both preflight and ordinary responses with curl.
Need a server to try this on? Antihost offers Linux VPS plans with NVMe storage, full root access and a 14-day free trial.
Nginx can own the policy instead when one proxy must apply the same rules to several upstream services. The important rule is to choose one policy owner. If Express and Nginx emit different Access-Control-Allow-* values, browsers can reject the response even though the application and proxy are each configured correctly in isolation.
As an Amazon Associate I earn from qualifying purchases.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
What CORS does—and what it does not do
Cross-Origin Resource Sharing (CORS) is an HTTP-header mechanism that lets a server tell a browser which other origins may load its resources. An origin is the combination of scheme, host, and port. For example, https://frontend.example.com and http://frontend.example.com are different origins, as are ports 443 and 8443 on the same host.
The browser enforces the policy. A command-line client, server-to-server request, or mobile app may receive the same HTTP response without enforcing CORS in the browser’s way. CORS therefore controls browser access; it is not authentication, authorization, encryption, or a substitute for session and API security.
#1 Best Overall
A cross-origin request may be sent directly, or the browser may first send a preflight request. A preflight uses OPTIONS and normally includes Origin, Access-Control-Request-Method, and, when needed, Access-Control-Request-Headers. The response must explicitly authorize the requested method and non-safelisted headers before the browser sends the actual request.
Choose the single owner of your CORS policy
Both Express and Nginx can set CORS response headers. Decide where the allowlist, credential behavior, and preflight handling belong before editing either configuration.
Recommended Free Tools
| Consideration | Express-owned policy | Nginx-owned policy |
|---|---|---|
| Best fit | One API or application-aware rules | Several upstream services sharing one proxy policy |
| Origin decisions | Can use static values or a dynamic application callback | Centralized at the proxy; keep the allowlist explicit |
| Preflight termination | Express middleware or an explicit route handler | Nginx can return a response before proxying |
| Application context | Can vary policy by route, tenant, or application state | Does not inherently know application authorization state |
| Main risk | Requests may miss the middleware if route order is wrong | Duplicate or conflicting headers if the upstream also emits CORS |
For a typical Express API, the Express option is easier to keep synchronized with application routes and authentication. Use Nginx as the owner when the proxy must enforce one consistent policy across multiple backends. Do not let both layers independently add the same headers.
Prerequisites on Ubuntu
- An Ubuntu server with Nginx installed and a server block for the API hostname.
- A Node.js application listening on a local address and port, such as
127.0.0.1:3000. - A frontend origin written exactly as the browser sends it, including scheme and, when non-standard, port.
- Administrative access for checking and reloading Nginx.
Replace https://frontend.example.com, api.example.com, and 127.0.0.1:3000 below with your actual values. Do not add a trailing slash to an origin value.
Configure CORS in Express
Install the middleware
npm install cors
Express’s cors middleware sets CORS response headers and can handle preflight requests. Put application-level middleware before the routes it must protect.
Rank #2
Use an explicit production origin
import express from 'express';
import cors from 'cors';
const app = express();
const corsOptions = {
origin: 'https://frontend.example.com',
methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
optionsSuccessStatus: 204
};
app.use(cors(corsOptions));
app.get('/api/health', (_req, res) => res.json({ ok: true }));
app.listen(3000);
This configuration allows the listed methods and request headers, authorizes the one named frontend, and tells the browser that credentialed requests are permitted. The 204 option makes successful preflight responses empty, which is a conventional response for OPTIONS.
Free tools Windows power users keep installed
One-click scans. No signup required.
If your application does not use cookies, HTTP authentication, or another browser credential, set credentials to false or omit it. Do not enable credentials merely because the API uses an Authorization header; decide based on whether the browser must send credentials under the Fetch specification.
Public, non-credentialed APIs
An API intended for browser access from any origin and not using credentialed browser requests can use a wildcard:
app.use(cors({ origin: '*' }));
The wildcard cannot be combined with credentialed browser requests. If the browser sends credentials, return the exact requesting origin instead of *, together with Access-Control-Allow-Credentials: true.
Limit CORS to selected routes
If only a subset of endpoints is public to another origin, apply the middleware to those routes rather than the entire application:
const apiCors = cors({
origin: 'https://frontend.example.com',
methods: ['GET', 'POST'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true
});
app.get('/api/health', apiCors, (_req, res) => {
res.json({ ok: true });
});
Route-level configuration reduces the chance that an unrelated endpoint receives a broader policy. Keep the route’s preflight handling aligned with the route itself.
Handle preflight explicitly only when necessary
Application-level app.use(cors(corsOptions)) handles preflight for the application. If you deliberately use route-level CORS and the global middleware is absent, add an explicit OPTIONS handler before the route:
app.options('/api/health', cors(corsOptions));
A preflight must not be forced through authentication that rejects OPTIONS before CORS runs. If authentication middleware is required globally, allow the preflight path to reach CORS first or terminate it at Nginx.
Configure Nginx as a reverse proxy
Proxy requests without changing CORS
When Express owns the policy, keep the Nginx server block focused on proxying and forwarding the request context:
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Host preserves the public hostname. The forwarded headers carry the client address and original scheme so the Node.js process can make correct URL, redirect, logging, and security decisions. The proxy does not need to rewrite the Origin header; the browser’s origin should reach Express unchanged.
Rank #4
Let Nginx own a single known-origin policy
If Nginx is the deliberate policy owner, add headers in the relevant location and remove equivalent CORS output from Express. This example authorizes one frontend and terminates its preflight:
location / {
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin "https://frontend.example.com" always;
add_header Access-Control-Allow-Methods "GET, HEAD, PUT, PATCH, POST, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
add_header Access-Control-Allow-Credentials "true" always;
add_header Content-Length 0;
add_header Content-Type text/plain;
return 204;
}
add_header Access-Control-Allow-Origin "https://frontend.example.com" always;
add_header Access-Control-Allow-Credentials "true" always;
add_header Vary Origin always;
proxy_pass http://127.0.0.1:3000;
}
The narrow if shown here only checks the request method and immediately returns a response. Adapt it to the rest of the server configuration rather than copying it into unrelated rewrite logic. The allowlist is explicit; do not reflect an arbitrary Origin value into Access-Control-Allow-Origin.
Nginx’s add_header directive is valid in http, server, and location contexts. Without always, Nginx emits a configured header only for its eligible response statuses, including common success and redirect codes. Adding always also emits it on otherwise excluded statuses, which is important when an error or redirect must still expose the CORS result to the browser. Header inheritance matters: defining add_header at a lower level changes which inherited headers apply, so inspect the effective location rather than assuming a server-level setting is active everywhere.
Vary: Origin tells caches that the response can differ by the request’s origin. Keep it when the allowed origin is selected dynamically or when a shared cache could otherwise serve one origin’s response to another.
Apply the configuration safely on Ubuntu
- Save the Nginx server block in the enabled site configuration used by your API hostname. Confirm that the
locationreceiving/api/...requests is the one containing the proxy and, if applicable, the CORS directives. - Check syntax before reloading:
sudo nginx -t
- Reload Nginx without stopping existing connections:
sudo systemctl reload nginx
- Confirm that the Node.js process is listening on the address and port named by
proxy_pass. A valid Nginx configuration cannot compensate for an upstream process that is stopped or listening elsewhere. - Open the frontend over its real scheme and hostname. Test the public API hostname, not the local Node.js port, because the browser evaluates the response that came through the proxy.
If you change only Express configuration, restart or reload the Node.js process using the process manager used by your deployment. If you change Nginx, always run the syntax check before the reload.
Verify preflight and actual responses
Test the preflight
curl -i -X OPTIONS https://api.example.com/api/health
-H 'Origin: https://frontend.example.com'
-H 'Access-Control-Request-Method: GET'
-H 'Access-Control-Request-Headers: Authorization, Content-Type'
For a successful credentialed policy, the response should include the exact origin, an Access-Control-Allow-Methods value containing GET, an Access-Control-Allow-Headers value containing both requested headers, and Access-Control-Allow-Credentials: true. A 204 status is expected in the configurations above, but another successful status can work if the required headers are present.
Test the actual request
curl -i https://api.example.com/api/health
-H 'Origin: https://frontend.example.com'
Check that the ordinary response also carries Access-Control-Allow-Origin. If credentials are used, check the credentials header as well. A successful preflight does not guarantee that the actual route response includes CORS headers; both paths must be covered.
Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchWindows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallUse browser diagnostics
In browser developer tools, open the Network panel and inspect the OPTIONS request followed by the actual request. The Console usually identifies the missing or mismatched header. Compare the browser’s exact Origin, requested method, and requested headers with the values in your policy. Non-browser clients do not reproduce the browser’s enforcement behavior, so a successful server-side request alone does not prove that frontend JavaScript can read the response.
Understand the headers that must agree
| Header | Purpose | Configuration implication |
|---|---|---|
Origin |
Identifies the calling page’s scheme, host, and port | Match the complete value; do not compare only the hostname |
Access-Control-Allow-Origin |
Names an allowed origin | Use the exact origin for credentialed requests; use * only for non-credentialed access |
Access-Control-Request-Method |
Method the browser wants to use after preflight | Include that method in Access-Control-Allow-Methods |
Access-Control-Request-Headers |
Non-safelisted headers the browser wants to send | Authorize every requested header in Access-Control-Allow-Headers |
Access-Control-Allow-Credentials |
Allows credentialed browser requests | Return true only when the application needs it, never with a wildcard origin |
Vary: Origin |
Separates cached responses by origin | Use when the response policy varies with the request origin |
Troubleshoot the failure that you actually see
| Symptom | Likely cause | Correction |
|---|---|---|
No Access-Control-Allow-Origin |
The request reached a different Express route or Nginx location, or a redirect/error response omitted the header | Inspect the public URL and effective location. Add always to Nginx headers where appropriate and ensure the intended Express middleware runs on the response path. |
OPTIONS returns 404, 405, or 401 |
Preflight is reaching a route that does not accept it, or authentication rejects it before CORS | Use application-level cors(), add an explicit app.options() handler before the route, or terminate the preflight in Nginx. |
| Credential error mentioning a wildcard | The response uses Access-Control-Allow-Origin: * while the browser sends credentials |
Return the exact scheme/host/port origin and Access-Control-Allow-Credentials: true, or remove credentialed requests. |
| Method not allowed by CORS | The requested method is absent from the allow-methods value | Read Access-Control-Request-Method and add that method to the policy if it is genuinely needed. |
| Header not allowed by CORS | A requested non-safelisted header is absent from the allow-headers value | Compare Access-Control-Request-Headers with the policy and authorize each required header. |
| Duplicate CORS headers | Express and Nginx both emit values, or they emit different values | Choose one owner and remove the other layer’s CORS additions. Conflicting duplicate values can cause browser rejection. |
| Express sees the wrong scheme or client address | The trust setting does not match the Nginx topology or forwarded headers | Review the proxy chain and configure Express trust proxy to trust only the real proxy arrangement. |
Works with curl but fails in the browser |
The browser is enforcing CORS, while the command-line client is not doing equivalent enforcement | Inspect the browser’s Network and Console panels, including the preflight and the final response. |
Set Express trust proxy deliberately
Nginx forwards the original scheme and client information through headers such as X-Forwarded-Proto, X-Forwarded-For, and X-Real-IP. Express must be told how much of that chain is trusted. The correct trust proxy value depends on your real topology: a single local Nginx process, multiple known proxies, or a load balancer followed by Nginx are different arrangements.
Do not enable a broad trust setting simply to make a redirect or secure-cookie check pass. Align the setting with the proxies you operate and the headers they actually overwrite. A mismatch can make Express believe an untrusted client supplied a forwarded scheme or address.
Production security checklist
- Write each allowed origin as a complete scheme, host, and port value.
- Prefer a small allowlist to reflecting whatever appears in
Origin. - Enable credentials only for browser flows that need them.
- Never pair credentialed requests with
Access-Control-Allow-Origin: *. - Allow only the methods and headers the API uses.
- Ensure preflight can complete without being blocked by authentication or route order.
- Keep CORS ownership in one layer to prevent duplicate headers.
- Use
Vary: Originwhen responses vary by origin and may be cached. - Review error and redirect responses, not just successful JSON responses.
- Keep Nginx’s forwarded-header and Express
trust proxysettings consistent with the actual network path.
Which arrangement should you use?
Use Express-owned CORS when
- The API is primarily an Express application.
- Different routes or tenants need different origin decisions.
- The application already knows whether a request is authenticated or otherwise eligible.
- You want the middleware to handle preflight beside the routes it protects.
Use Nginx-owned CORS when
- Several upstream services require one uniform browser policy.
- You intentionally want preflight rejected or accepted before it reaches application code.
- The allowlist is stable and can be represented in the proxy configuration.
In either arrangement, test the public hostname with both an OPTIONS request and the actual method. The browser needs matching headers on the response it receives through Nginx, not merely on a local response from Node.js.
Do these 3 things before closing this tab:
1Scan for outdated or missing drivers - takes under a minute2Repair Windows errors before they cause bigger problems3Fix the driver behind crashes, sound loss and screen glitchesQuick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




