iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more
Need a server to try this on? Antihost offers Linux VPS plans with NVMe storage, full root access and a 14-day free trial.
Configure CORS in one deliberate place—usually Express—then let Nginx proxy the API unchanged. Install the cors middleware, allow the exact frontend origin, make sure preflight OPTIONS requests reach the middleware, preserve forwarded headers in Nginx, reload safely, and verify both preflight and actual responses with curl. Use Nginx as the policy owner only when a single, consistent rule must cover several upstream services.
What CORS does in a reverse-proxied application
Cross-Origin Resource Sharing (CORS) is an HTTP-header mechanism enforced by browsers. A browser compares the page’s origin (scheme, host, and port) with the API response. The API must explicitly indicate which origins, methods, and request headers are allowed. CORS controls whether browser JavaScript may read a response; it is not authentication or authorization. An attacker who obtains an API credential can still call the API from a non-browser client, so your application must enforce identity and permissions separately.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
With Nginx in front, the request path normally looks like this:
- The browser sends a request to the public API hostname.
- Nginx accepts the connection and proxies it to Node.js, commonly on
127.0.0.1:3000. - Express handles the route and may add CORS response headers.
- Nginx returns the upstream response to the browser.
For a non-simple request, the browser first sends a preflight OPTIONS request. It includes Origin, Access-Control-Request-Method, and, when needed, Access-Control-Request-Headers. The preflight response must authorize every requested method and non-safelisted header before the browser sends the real request.
#1 Best Overall
Choose one owner for the CORS policy
Most Express APIs should keep the allowlist and credential decisions in Express. Application code can make origin decisions using tenant data, environment variables, or route-specific rules, and the same policy follows the application if the proxy changes. Configure Nginx only as a transport proxy in that design.
Nginx can own CORS when one gateway policy must cover several upstream services. This centralizes behavior, but Nginx cannot naturally apply application-aware decisions. Do not let both layers emit independent values: duplicate or conflicting Access-Control-Allow-* headers can cause the browser to reject the response.
| Decision point | Express-owned policy | Nginx-owned policy |
|---|---|---|
| Where origins are evaluated | In JavaScript, with static or dynamic rules | In the reverse-proxy configuration |
| Best fit | One API with route- or user-aware rules | Several services sharing one gateway rule |
| Where preflight ends | Express middleware or an explicit OPTIONS route |
Nginx can return 204 before proxying |
| Main risk | Middleware mounted after routes or omitted on a route | Conflicting headers if upstream also emits CORS |
| Operational requirement | Deploy policy with the application | Reload and validate Nginx after policy changes |
Option A: configure CORS in Express
Install the middleware
From the Node.js application directory, install the Express CORS middleware:
npm install cors
The package sets CORS response headers and can handle preflight requests. Mount it before routes so an OPTIONS request is processed before a route-level 404, 405, or authentication guard.
Use an explicit production origin
This example permits one frontend, the methods used by the API, and the request headers commonly sent by browser clients. It enables credentials because the application is assumed to use cookies or another browser-managed credential. Remove credentials when the API is intentionally anonymous.
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);
The value of origin must match the browser’s origin exactly, including scheme and, when present, port. Do not add a trailing slash to an origin value. If the site is served from another environment, define the origin through environment-specific configuration rather than accepting arbitrary input.
Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteWindows 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 reinstallRank #2
Allow a public, non-credentialed API
For an API that does not use cookies or other credentialed browser requests, a wildcard can be appropriate:
app.use(cors({ origin: '*' }));
A wildcard cannot be combined with credentialed requests. Browsers require an explicit origin when credentials are involved, so replace * with the exact frontend origin and send Access-Control-Allow-Credentials: true only when the application needs it.
Use dynamic or route-specific rules deliberately
Express also supports a function for origin decisions. A dynamic rule is useful when a controlled list comes from configuration or a database, but it must reject unknown origins rather than reflect every value supplied by the client. Keep the callback’s allowlist logic in application code and test rejected origins as well as accepted ones.
If only selected endpoints should be cross-origin, apply the middleware to those routes instead of the whole application. When global middleware is not suitable, register an explicit app.options('/route', cors()) handler for the affected route and ensure it runs before authentication and route handlers.
The Tool Desk
Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Account for the real proxy topology
Nginx forwards the original host, client address, and scheme in the configuration shown later. Express must trust those forwarded values only to the degree that matches your topology. For a single, controlled Nginx hop, an application may use:
app.set('trust proxy', 1);
The number or function must reflect the actual number of trusted proxies and their network boundaries. An overly broad setting can let an untrusted client spoof forwarded information; an overly narrow setting can make Express see the wrong protocol or client IP. Review this setting whenever another load balancer or ingress layer is added.
Configure Nginx to proxy the Node.js app
Transport-only server block
When Express owns CORS, keep the Nginx server block focused on proxying. This preserves the public host and forwards connection metadata that Express and application logs 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;
}
}
With this arrangement, do not add a second set of CORS headers in Nginx. Requests, including OPTIONS, are proxied to Express and receive one authoritative response.
Nginx as the policy owner
If Nginx must enforce the policy for several upstream services, add headers in the relevant location. The following narrow example allows one known frontend and returns a successful preflight without contacting Node.js:
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 if block is intentionally limited to testing the request method and returning a response. Adapt it to the rest of your server configuration, especially if other locations, authentication rules, or redirects are present. Use an allowlist; never 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, headers are emitted only for a defined set of response codes, including common successful responses and redirects. Adding always makes the headers available on otherwise excluded error responses, which helps the browser expose a useful failure to your client code. Header inheritance also matters: defining add_header in a nested context changes which parent headers are inherited, so inspect the effective location rather than assuming a server-level rule applies everywhere.
If Express currently emits CORS headers, remove those headers from Express before making Nginx the owner, or leave Nginx as a transport-only proxy. A response with two different allow-origin values is not a valid compromise.
Recommended Free Tools
Rank #4
Deploy and reload safely on Ubuntu
Validate the configuration before reloading
After editing the active Nginx server configuration, ask Nginx to parse it:
sudo nginx -t
Only reload after the test reports a valid configuration:
sudo systemctl reload nginx
A reload applies the new configuration without the interruption associated with stopping the service. If the test fails, use the reported file and line number to correct the syntax, then run the test again. Keep the Node.js process listening on the address and port used by proxy_pass; a valid Nginx configuration cannot compensate for an unavailable upstream.
Check the public endpoint, not only localhost
Testing the Node.js port directly can bypass the proxy and hide problems with locations, redirects, forwarded headers, or Nginx response handling. Use the public API hostname in both tests below.
Do these 3 things before closing this tab:
1Repair Windows errors before they cause bigger problems2Scan for outdated or missing drivers - takes under a minute3Clear out junk files and repair common Windows errorsVerify preflight and actual requests
Inspect a preflight response
Send the same headers a browser would send for a cross-origin request that uses an authorization header:
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'
Confirm that the response contains an allow-origin value matching the supplied origin, an allow-methods value containing GET, and an allow-headers value containing both requested names. A policy that uses credentials must also return the credentials header with a non-wildcard origin. A successful Nginx-terminated preflight in the example returns status 204; an Express response may use the middleware’s configured success status.
Inspect the actual request
curl -i https://api.example.com/api/health
-H 'Origin: https://frontend.example.com'
The actual response also needs the allow-origin header. Browsers enforce CORS on the response they expose to JavaScript, so a preflight that succeeds does not prove that the subsequent route response contains the required headers.
Use browser diagnostics
Open the browser’s network panel and inspect both the OPTIONS request and the following request. Check the request’s Origin, the preflight’s requested method and headers, the response status, and every Access-Control-Allow-* header. The console often reports the first policy mismatch, while the network panel reveals redirects or error responses that are easier to miss in application logs.
Troubleshoot the common failure modes
| Symptom | Likely cause | Specific checks and remedy |
|---|---|---|
No Access-Control-Allow-Origin |
The request reached a different location, a redirect, or an error response that lacks the policy | Check the public URL and Nginx location selected for the path. Inspect the first response in the network panel. If Nginx owns the policy, use always on headers that must survive non-success responses. |
OPTIONS returns 404, 405, or 401 |
Preflight is being handled by a route or authentication layer that does not accept OPTIONS |
Mount Express’s application-level cors() before routes, add an explicit route-level OPTIONS handler, or terminate the preflight in Nginx. Do not require the browser to present application credentials before it is allowed to ask permission. |
| Credential error in the console | The response uses a wildcard origin or omits the credentials header | Use the exact scheme, host, and port of the frontend and enable credentials only in the layer that owns the policy. Ensure the client’s request mode and cookie settings match the server’s decision. |
| Method or header denied | The preflight requested a method or non-safelisted header not listed by the server | Read Access-Control-Request-Method and Access-Control-Request-Headers from the request, then add only the required values to the corresponding allow lists. |
| Duplicate or conflicting CORS headers | Both Express and Nginx are adding policy headers, or nested Nginx contexts define different values | Choose one owner, remove the other layer’s additions, and inspect the final wire response with curl -i. |
| Express reports the wrong scheme or client address | The trust-proxy setting does not match the actual proxy chain or forwarded headers | Compare the topology with the X-Forwarded-* headers Nginx sends. Set the smallest trust rule that covers your known proxies and retest redirects, secure-cookie logic, and logs. |
Works with curl but fails in a browser |
CORS is a browser enforcement mechanism, and the browser may be issuing a preflight that the manual request did not reproduce | Capture the browser’s exact origin, requested method, and requested headers. Re-run the preflight command with those values and inspect the browser console for the first rejected response. |
Production checklist
- Write down the single CORS owner: Express or Nginx.
- Allow exact production origins; keep development origins separate from production configuration.
- Use credentials only when the application needs browser-managed credentials, and never pair them with a wildcard origin.
- List the methods and request headers the frontend actually uses, including authorization and content type when applicable.
- Ensure preflight handling runs before route authentication and before a catch-all 404 handler.
- Preserve
Host,X-Real-IP,X-Forwarded-For, andX-Forwarded-Protowhen proxying. - Set Express’s trust-proxy behavior to the real number and identity of trusted proxy hops.
- Return
Vary: Originwhen responses vary by origin, so shared caches do not serve one origin’s response to another. - Use Nginx’s
alwaysparameter when policy headers must appear on error responses. - Run
sudo nginx -tbefore every reload, then verify bothOPTIONSand the actual request through the public hostname. - Treat CORS as a browser access policy, not as a substitute for authentication, authorization, rate limiting, or input validation.
Which configuration should you use?
Use Express when the API owns the decision
Choose Express for a single Node.js API, route-specific access, tenant-aware origin checks, or a policy that belongs in the same deployment as the application. The middleware handles preflight and actual responses consistently, and a dynamic callback can consult controlled application configuration.
Use Nginx when the gateway owns the decision
Choose Nginx when multiple upstream services must share one simple allowlist and you want preflight requests rejected or completed before they consume application resources. Keep the rule centralized, document which locations inherit it, and remove upstream CORS headers to prevent conflicts.
Do not split one policy across both layers
Splitting responsibility—for example, origins in Express but credentials and methods in Nginx—makes the final response difficult to reason about. If requirements change, update the chosen owner, reload or redeploy that layer, and repeat the public preflight and actual-request checks.
Bottom line
For most Node.js applications behind Nginx on Ubuntu, install and configure Express’s cors middleware with an explicit origin, make sure it handles OPTIONS before routes, and use Nginx only to proxy and forward request metadata. If Nginx must serve several upstreams under one gateway policy, terminate preflight there, add headers with always, allowlist origins, and disable duplicate CORS output from Express. Validate the effective public response—not just the local Node.js process—before declaring the browser integration fixed.
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.

