Driver FixRecommendedSound, Wi-Fi or graphics acting up? Check drivers firstFind missing or outdated drivers fast.Check DriversHalloween DealsAmazon USHalloween week deals: useful picks before month-endAmazon US: current deals, useful picks and tech finds.Check DealsSlow PC?RecommendedPC slow today? Run a repair scan before it gets worseResolve common Windows issues and optimize system performance.Scan Now×
Skip to content
World desk10 min

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

A practical guide to enabling CORS for an Express API behind Nginx on Ubuntu, including single-owner architecture, preflight handling, credential rules, proxy settings, verification commands, and troubleshooting.
Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

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.

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

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.

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.

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

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.

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

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:

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

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

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.

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

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

  1. Save the Nginx server block in the enabled site configuration used by your API hostname. Confirm that the location receiving /api/... requests is the one containing the proxy and, if applicable, the CORS directives.
  2. Check syntax before reloading:
sudo nginx -t
  1. Reload Nginx without stopping existing connections:
sudo systemctl reload nginx
  1. 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.
  2. 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.

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

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

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

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: Origin when 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 proxy settings 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.

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.

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 from the Wire

  1. World desk4 min
    How to Spot an AI Voice Scam Before Sending MoneyDon’t rely on how a caller sounds. Pause, call back through a known number, and verify the emergency with another trusted person before sending money.
  2. Mountain View desk4 min
    Google’s SynthID Detector: How to Check AI-Generated Images, Video and AudioGoogle’s SynthID Detector looks for an embedded watermark in supported images, video and audio. Here is what its results do—and do not—show.
  3. Redmond desk20 min
    How to create a link to File or Folder in Windows 11Windows 11 gives you several ways to point to a file or folder without moving or duplicating it. You can create a desktop shortcut,…
Recommended PC Tool
Recommended PC Tool
Crashes, No Sound, or Screen Glitches?Free driver scan
PC Slower Than It Used to Be?Free scan - under a minute

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.