Hardware FixRecommendedDevice not working? Your driver may be the problemCheck updates for common hardware issues.Fix DriversHalloween DealsAmazon USHalloween week deals: useful picks before month-endAmazon US: current deals, useful picks and tech finds.Check DealsClean PCRecommendedOne scan can reveal what keeps slowing WindowsLook for cleanup and repair opportunities.Run Scan×
Skip to content
VGSources
Blog

How to Enable CORS for a Node.js App Behind NGINX on Ubuntu

A practical guide to configuring Express or NGINX CORS behind an Ubuntu reverse proxy, with preflight commands, secure credential rules, and troubleshooting.
Length11 min Posted Quest giverVGSources Team
Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

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.

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

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.

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.

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

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 Origin header.

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:

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

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.

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

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.

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

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.

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

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

  1. Place the server block in the Ubuntu NGINX configuration layout used by your installation and enable it according to that layout.
  2. Check syntax before touching the running service:
    sudo nginx -t

    Continue only when the test reports a successful configuration.

  3. Reload without dropping existing connections:
    sudo systemctl reload nginx
  4. If the reload fails, read the service and NGINX error logs, correct the reported directive or context, run sudo nginx -t again, 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.

Test the preflight and the real request

Send a representative preflight

Use the same origin, method, and non-safelisted headers your frontend sends:

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

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.

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 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 always to the relevant directives.
  • Verify that the incoming Origin exactly matches the configured scheme, host, and port.

OPTIONS returns 404, 405, or 401

  • With Express ownership, register application-level cors() or an explicit app.options() handler before routes and middleware that reject the request.
  • With NGINX ownership, terminate the preflight in the matching location and return 204, or proxy it to an Express handler that is known to accept OPTIONS.
  • Do not assume a route that supports GET automatically supports OPTIONS; 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.

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

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 Origin header.
  • 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: Origin whenever the response changes according to the requesting origin.
  • Use NGINX’s always option 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 proxy only 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.

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

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.

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.

Leave a Reply

Your email address will not be published. Required fields are marked *

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

More quests from Patch Notes

  1. How to Set Up a RedM (RDR2) Server in 2026: License Key, txAdmin and server.cfg, Step by StepBlog11min
  2. Best RedM (RDR2) Server Hosting in 2026: Comparing Four Hosts on Slots, Memory and PriceBlog10min
  3. How to Host a Mindustry Server in 2026: server-release.jar, Port 6567 and the Commands That MatterBlog7min
Recommended PC Tool
Recommended PC Tool
Windows Errors? Fix Them Before They SpreadFree repair scan
Crashes, No Sound, or Screen Glitches?Free driver scan

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.