Recommended Free Tools
For most Express APIs, configure CORS in Express and let NGINX proxy requests unchanged. Install the cors middleware, allow the exact frontend origin (including scheme and port), authorize the methods and headers your browser requests, and make sure OPTIONS preflight requests reach that middleware. Use NGINX as the CORS owner only when you deliberately want one policy shared across several upstream services; never emit competing CORS headers from both layers.
Need a server to try this on? Antihost offers Linux VPS plans with NVMe storage, full root access and a 14-day free trial.
The guide below shows both designs, a working Ubuntu reverse-proxy configuration, verification commands, and fixes for the preflight errors that commonly appear only after an app is placed behind NGINX.
As an Amazon Associate I earn from qualifying purchases.
What CORS controls
Cross-Origin Resource Sharing (CORS) is an HTTP-header mechanism enforced by web browsers. A browser compares the origin of the page making a request with the response headers from the API. An origin is the combination of scheme, host, and port: https://frontend.example.com, https://frontend.example.com:8443, and http://frontend.example.com are different origins.
The API signals its decision with response headers such as Access-Control-Allow-Origin, Access-Control-Allow-Methods, and Access-Control-Allow-Headers. CORS is not authentication or authorization. A script, command-line client, game launcher, or server-side program can call your endpoint without obeying browser CORS rules, so protect the API with normal authentication, authorization, rate limiting, and input validation as well.
#1 Best Overall
Choose one place to own the policy
Both Express and NGINX can add CORS headers. Pick one owner for each API response. If both layers add the same header, the browser can receive duplicate or conflicting values and reject the response.
| Consideration | Express owns CORS | NGINX owns CORS |
|---|---|---|
| Best fit | Most Express APIs, especially when the decision depends on application data or a tenant allowlist. | A gateway policy that must apply consistently to several upstream services. |
| Origin decisions | Supports a static origin or a dynamic callback that checks an allowlist in application code. | Convenient for a small, stable set of origins configured at the proxy. |
| Preflight location | Express middleware answers OPTIONS before route handlers. |
NGINX can terminate OPTIONS and return a 204 response without contacting Node.js. |
| Application awareness | Can vary policy by route, user, tenant, or other request context. | Sees proxy-level request data, not your application’s authorization state. |
| Main risk | Routes or authentication middleware may run before CORS and reject preflight. | Configuration drift across locations and accidental duplicate headers from Express. |
For a single Express API, the first column is generally the least surprising arrangement: keep the allowlist and credential decision next to the routes, and leave NGINX responsible for transport and forwarding.
Prerequisites and the request path
Assume the Node.js process listens on 127.0.0.1:3000, NGINX serves api.example.com, and the browser application is hosted at https://frontend.example.com. Substitute your real values exactly. A trailing slash is not part of an origin, but a different scheme, hostname, or port is.
- A running Node.js application with an API route such as
/api/health. - NGINX installed on an Ubuntu server and a server block for the API hostname.
- DNS pointing the API hostname to that server and a firewall rule allowing the listener port.
- The frontend origin written exactly as the browser sends it in the
Originheader.
The browser sends requests to NGINX. NGINX forwards ordinary requests to Node.js. For a non-simple cross-origin request, the browser first sends an OPTIONS preflight containing Origin, Access-Control-Request-Method, and, when needed, Access-Control-Request-Headers. The response must explicitly authorize the requested method and every requested non-safelisted header.
Configure CORS in Express
Install the middleware
From the application directory, install the maintained Express middleware:
npm install cors
Use an explicit production policy
This example allows one frontend, common API methods, and the headers used by a JSON client. It also enables credentials for cookie- or client-certificate-based browser requests:
The Tool Desk
Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →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, '127.0.0.1');
Register app.use(cors(corsOptions)) before your routes. Application-level use handles preflight for all routes. The 204 success status is an empty, successful preflight response and is understood by current browsers.
Rank #2
When only selected routes are public
If the entire application should not be cross-origin, attach CORS to specific routers or routes instead of globally:
import cors from 'cors';
const publicCors = cors({
origin: 'https://frontend.example.com',
methods: ['GET'],
allowedHeaders: ['Content-Type']
});
app.get('/api/catalog', publicCors, catalogHandler);
When global middleware is not appropriate, add an explicit preflight handler for the route before the route’s normal handler:
app.options('/api/catalog', cors({
origin: 'https://frontend.example.com',
methods: ['GET'],
allowedHeaders: ['Content-Type']
}));
Do not put authentication that requires a bearer token or session cookie in front of preflight unless it is designed to let OPTIONS through. A preflight is permission negotiation; it does not carry the application request’s body or credentials in the way the subsequent request does.
Public APIs without credentials
If an endpoint is intentionally public and does not use cookies or other browser credentials, a wildcard policy is possible:
app.use(cors({ origin: '*' }));
Never combine Access-Control-Allow-Origin: * with credentialed browser requests. For a credentialed request, the response must name the exact requesting origin and include Access-Control-Allow-Credentials: true. Keep the wildcard limited to endpoints whose exposure is deliberate.
Dynamic origin checks
Express also supports an origin callback. Use it to compare the incoming origin with a server-side allowlist, not to reflect every value supplied by the client:
const allowedOrigins = new Set([
'https://frontend.example.com',
'https://staging-frontend.example.com'
]);
const corsOptions = {
origin(origin, callback) {
if (!origin || allowedOrigins.has(origin)) {
callback(null, true);
} else {
callback(new Error('Origin is not allowed'));
}
},
credentials: true
};
app.use(cors(corsOptions));
The !origin branch permits requests without an Origin header, which commonly includes non-browser health checks and command-line clients. It does not make those clients subject to browser CORS enforcement; authentication still belongs elsewhere.
Free tools Windows power users keep installed
One-click scans. No signup required.
Configure NGINX as the reverse proxy
With Express as the policy owner, NGINX only forwards the request and preserves the information the application may need:
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. X-Real-IP and X-Forwarded-For carry client-address information, while X-Forwarded-Proto tells the application whether the original request used HTTP or HTTPS. Only trust these headers when they are set by a proxy you control.
Let NGINX own CORS when a shared proxy policy is intentional
If several upstream applications need one identical policy, or if you want NGINX to answer preflight without waking Node.js, put the headers in the relevant location. This narrow example allows one known frontend:
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 origin-specific response includes Vary: Origin so an intermediary does not reuse a response authorized for one origin when serving another. The always parameter makes NGINX add the header to response statuses that would otherwise be excluded, including many redirects and errors. Without it, a browser may report a CORS failure for an upstream 401, 404, or 500 even though the real problem is the underlying status.
NGINX’s add_header directive is valid in http, server, and location contexts. It normally emits headers only for selected success and redirect statuses, including 200, 201, 204, 206, 301, 302, 303, 304, 307, and 308; always extends emission to other statuses. Header inheritance also matters: adding headers at one level can change which inherited add_header directives apply in a child location, so inspect the complete active configuration rather than only the file you edited.
If Express already sends CORS headers, remove the NGINX additions or remove the Express policy. Do not try to “strengthen” a response by sending two Access-Control-Allow-Origin fields.
Apply the configuration safely on Ubuntu
- Place the server block in the Ubuntu NGINX configuration layout used by your installation and enable it according to that layout.
- Check syntax before touching the running service:
sudo nginx -tContinue only when the test reports a successful configuration.
- Reload without dropping existing connections:
sudo systemctl reload nginx - If the reload fails, read the service and NGINX error logs, correct the reported directive or context, run
sudo nginx -tagain, and reload only after the test passes.
These commands validate configuration syntax and apply it; they do not prove that the browser sees the intended headers. Test the public hostname after reloading.
Rank #4
Test the preflight and the real request
Send a representative preflight
Use the same origin, method, and non-safelisted headers your frontend sends:
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 reinstallcurl -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'
A successful response should have a 2xx status (204 is typical) and include an exact Access-Control-Allow-Origin value, an Access-Control-Allow-Methods value containing GET, and an Access-Control-Allow-Headers value containing both Authorization and Content-Type. If credentials are used, it also needs Access-Control-Allow-Credentials: true.
Inspect the actual request
curl -i https://api.example.com/api/health
-H 'Origin: https://frontend.example.com'
Check that the actual response—not only the preflight—contains the required allow-origin header. An API can answer preflight correctly yet omit CORS headers on a 401, 404, redirect, or application exception.
Compare with the browser
Open the browser’s developer tools, select the failed request in the Network panel, and inspect both the OPTIONS request and the follow-up request. The Console usually identifies the exact missing permission, such as a denied method, a denied request header, a wildcard used with credentials, or a missing allow-origin header. A successful curl call does not by itself prove browser success because CORS is a browser-enforced policy.
How preflight authorization is evaluated
| Browser request item | What the server must do |
|---|---|
Origin |
Return that exact allowed origin, or a wildcard only when the request is not credentialed. |
Access-Control-Request-Method |
List the requested method in Access-Control-Allow-Methods. |
Access-Control-Request-Headers |
List every requested non-safelisted header in Access-Control-Allow-Headers; header-name matching is case-insensitive. |
| Credentials mode | Return Access-Control-Allow-Credentials: true and never use * for the origin. |
Browsers commonly trigger preflight for methods such as PUT, PATCH, and DELETE, or when JavaScript sets headers such as Authorization or a JSON Content-Type. A simple request may skip preflight, but its response still needs a valid Access-Control-Allow-Origin header for browser JavaScript to read it.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Troubleshoot the failure that you actually have
No Access-Control-Allow-Origin header
- Confirm the request reached the intended Express route or NGINX
location; a different hostname, path, or location block may be serving it. - Check for an HTTP-to-HTTPS redirect. The redirect response and the final response must both be usable by the browser, and the frontend origin must use the final scheme.
- Check error responses as well as 2xx responses. In NGINX-owned configurations, add
alwaysto the relevant directives. - Verify that the incoming
Originexactly matches the configured scheme, host, and port.
OPTIONS returns 404, 405, or 401
- With Express ownership, register application-level
cors()or an explicitapp.options()handler before routes and middleware that reject the request. - With NGINX ownership, terminate the preflight in the matching
locationand return 204, or proxy it to an Express handler that is known to acceptOPTIONS. - Do not assume a route that supports
GETautomatically supportsOPTIONS; preflight is a separate request.
Credentials error or wildcard rejection
Replace * with the exact frontend origin, add Access-Control-Allow-Credentials: true only when the application needs credentials, and ensure the browser request itself is configured to include them. A wildcard cannot authorize a credentialed browser request.
Method or header denied
Read the browser’s Access-Control-Request-Method and Access-Control-Request-Headers values. Add every requested method and non-safelisted header to the corresponding allow list. For example, a policy that allows Content-Type but not Authorization will fail when the frontend sends a bearer token.
Duplicate or conflicting headers
Inspect the raw response with curl -i and choose Express or NGINX as the sole policy owner. Remove one set of directives, then reload NGINX and restart or redeploy the Node.js process as appropriate. A proxy cannot reliably reconcile two different allow-origin values.
Only the browser fails
Command-line tools and server-to-server clients do not enforce CORS in the same way as browsers. Use the browser’s console and network trace to identify the policy mismatch, while separately checking authentication and API status codes.
Make Express understand the proxy topology
When TLS terminates at NGINX, Express may see an internal HTTP connection even though the user connected over HTTPS. The forwarded headers in the proxy block preserve the original scheme and client address, but Express must be configured to trust the proxy correctly:
app.set('trust proxy', 1);
The value must match the real topology. A single trusted proxy hop can be appropriate when every request reaches Express through one NGINX instance; a subnet, hop count, or custom function may be safer in a different deployment. Trusting arbitrary forwarded headers lets clients spoof scheme or address information. Review the Express proxy guidance and your actual load-balancer chain before choosing the setting. This setting does not create CORS permissions; it prevents incorrect protocol, secure-cookie, redirect, and client-IP decisions behind the proxy.
Production checklist
- Write down the permitted origins, methods, headers, and whether credentials are required.
- Use HTTPS origins in production and keep development origins separate.
- Allow only origins you control; do not reflect an arbitrary
Originheader. - Choose Express or NGINX as the owner and remove duplicate policy code.
- Ensure preflight reaches a handler that returns 2xx without requiring the normal application credentials.
- Include
Vary: Originwhenever the response changes according to the requesting origin. - Use NGINX’s
alwaysoption when browsers must receive CORS headers on errors or redirects. - Test both preflight and actual requests, including a denied origin and a request with every production header.
- Keep CORS separate from authentication, authorization, CSRF defenses, and rate limiting.
- Set Express
trust proxyonly for the proxies and forwarded headers you genuinely control.
Which design should you use?
Single Express API and one frontend
Use Express middleware with an explicit origin and let NGINX proxy without CORS directives. This keeps route policy, credentials, and any dynamic allowlist in one place.
Several services behind one gateway
NGINX ownership can reduce repeated configuration, provided every service is intended to share the same browser policy. Terminate preflight consistently and remove application-level CORS headers that would conflict.
Quick wins for a faster PC:
Clear out junk files and repair common Windows errorsFree Scan →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Repair Windows errors before they cause bigger problemsFix Now →Public, read-only endpoint
A wildcard may be acceptable when the endpoint is intentionally public and no browser credentials are involved. It still does not protect the endpoint from non-browser callers.
Cookie-based session API
Use a specific origin, enable credentials on the server, configure the frontend to send credentials, and never use a wildcard origin. Also apply your normal session and CSRF protections; CORS alone does not make a cookie-backed API safe.
Quick 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.




