Node.js Backend Fundamentals with Express · Data, Auth, Tests and Production

Security Headers, CORS and Rate Limiting

Add helmet for security headers, configure CORS for the browsers you trust, and rate-limit sensitive endpoints.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

An API with helmet security headers, CORS that allows only your frontend's origin, a request body size limit, and a rate limit on the login route that answers 429 after too many attempts.

The idea

  • helmet sets defensive HTTP headers (Content-Security-Policy, X-Content-Type-Options, Strict-Transport-Security...) and removes X-Powered-By.
  • CORS controls which browser origins may call your API with JavaScript; it is not authentication. Allow specific origins, not *, for authenticated APIs.
  • Rate limiting slows down brute force and abuse; apply stricter limits to login, password reset and expensive endpoints. Behind a proxy, set trust proxy so the client IP is correct.
  • Body size limits (express.json({ limit: "100kb" })) stop huge payloads from using memory.

The code

helmet, CORS and a login rate limit
import express from "express";
import helmet from "helmet";
import cors from "cors";
import { rateLimit } from "express-rate-limit";

const app = express();
app.use(helmet());
app.use(cors({ origin: ["https://app.example.com"] }));
app.use(express.json({ limit: "10kb" }));
const loginLimiter = rateLimit({ windowMs: 15 * 60 * 1000, limit: 3, standardHeaders: "draft-8", legacyHeaders: false });
app.post("/login", loginLimiter, (req, res) => res.status(401).json({ error: "wrong email or password" }));
app.get("/public", (req, res) => res.json({ ok: true }));
app.use((err, req, res, next) => res.status(err.status ?? 500).json({ error: err.type ?? "error" }));

const server = app.listen(0);
const base = `http://127.0.0.1:${server.address().port}`;
const r = await fetch(`${base}/public`, { headers: { Origin: "https://app.example.com" } });
for (const h of ["x-powered-by", "x-content-type-options", "strict-transport-security", "access-control-allow-origin"]) {
  console.log(`${h}: ${r.headers.get(h)}`);
}
const evil = await fetch(`${base}/public`, { headers: { Origin: "https://evil.example" } });
console.log("allowed origin for evil.example:", evil.headers.get("access-control-allow-origin"));
const statuses = [];
for (let i = 0; i < 5; i++) statuses.push((await fetch(`${base}/login`, { method: "POST" })).status);
console.log("login attempts:", statuses.join(" "));
const big = await fetch(`${base}/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ x: "a".repeat(20000) }) });
console.log("20 kB body:", big.status);
server.close();

Output

x-powered-by: null
x-content-type-options: nosniff
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-origin: https://app.example.com
allowed origin for evil.example: null
login attempts: 401 401 401 429 429
20 kB body: 413

Run this on your computer — see “Get Started” for setup.

Common mistakes

  • cors() with no options on an authenticated API, allowing every origin.
  • Believing CORS protects the API from non-browser clients; it doesn't.
  • Rate limiting by IP behind a load balancer without app.set("trust proxy", ...), so every client shares one limit.
  • In-memory rate limit stores with several instances; use Redis or the gateway's limits.

Key points

  • helmet for security headers; small body limits.
  • CORS lists the browser origins you trust; it isn't access control.
  • Rate-limit authentication and expensive routes, with a shared store across instances.

Further reading: Express security best practices · helmet · express-rate-limit

Practice questions

Does CORS protect an API from unauthorised clients?

Answer

No. CORS only tells browsers which origins may read responses from JavaScript; curl and servers ignore it. Use authentication and authorisation for access control.

Why set trust proxy when rate limiting behind a load balancer?

Answer

Otherwise req.ip is the load balancer's address, so all clients share one limit; trust proxy makes Express use X-Forwarded-For.

What does helmet do?

Answer

Sets security-related HTTP headers (CSP, HSTS, X-Content-Type-Options and others) and removes X-Powered-By.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?