Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

With Nginx in front, the request path normally looks like this:

  1. The browser sends a request to the public API hostname.
  2. Nginx accepts the connection and proxies it to Node.js, commonly on 127.0.0.1:3000.
  3. Express handles the route and may add CORS response headers.
  4. 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.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Verify 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

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, and X-Forwarded-Proto when proxying.
  • Set Express’s trust-proxy behavior to the real number and identity of trusted proxy hops.
  • Return Vary: Origin when responses vary by origin, so shared caches do not serve one origin’s response to another.
  • Use Nginx’s always parameter when policy headers must appear on error responses.
  • Run sudo nginx -t before every reload, then verify both OPTIONS and 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.