Securing Your API (Hardening)
A public API is a target the moment it goes live. Add the standard layers of protection every professional back-end ships with.
What you will learn
- Add secure HTTP headers with Helmet
- Throttle abusive traffic with rate limiting
- Configure CORS tightly and sanitize input against injection
Your API lives in a rough neighbourhood
The instant your API is reachable on the internet, automated bots start probing it — trying common attacks, hammering login routes, and looking for weak spots. You cannot stop them knocking, but you can make the door hard to break. Professional back-ends ship with several layers of defence, each cheap to add and each closing off a class of attack. We will add the four most important.
Layer 1 — secure headers with Helmet
Browsers respect special HTTP response headers that tighten security — telling the browser not to guess content types, not to embed your site in a hostile frame, and so on. Setting them by hand is fiddly, so the Helmet package sets a sensible bundle of them in one line:
npm install helmetNote: Output:
added 1 package in 1s
This downloads the helmet package into node_modules/ and records it in package.json, so you can require it in the next step.
const helmet = require("helmet");
app.use(helmet()); // sets ~15 protective headers automaticallyNote: Output:
(no visible output — headers are added to every response)
app.use(helmet()) runs on every request and attaches a set of well-tested security headers. There is nothing to configure to get started — it is one of the highest-value single lines you can add to an Express app.
Layer 2 — rate limiting (stop the spammers)
Without limits, a bot can fire thousands of requests per second — guessing passwords on your login route or simply overwhelming your server (a denial-of-service attack). Rate limiting caps how many requests one client (by IP address) may make in a time window, replying 429 (Too Many Requests) once they go over.
const rateLimit = require("express-rate-limit");
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per IP per window
message: { error: "Too many requests, please try again later" }
});
app.use(limiter); // apply to all routesHow it reads: windowMs is the length of the window (here 15 minutes, written as 15 * 60 * 1000 milliseconds), and max is how many requests one IP gets in that window. The 101st request inside 15 minutes is rejected with a 429 and your message, then the counter resets when the window rolls over.
Note: Output (the 101st request within 15 minutes from one IP):
{"error":"Too many requests, please try again later"}
Real apps often apply a stricter limiter just to sensitive routes like /login (say, 5 attempts per 15 minutes) to blunt password-guessing, plus a looser global one for everything else.
Layer 3 — CORS, done properly
In the earlier lesson you enabled CORS with app.use(cors()) — which allows requests from any origin. That is convenient for learning but means any website on the internet can call your API from a browser. In production you whitelist only your own front-end:
const cors = require("cors");
app.use(cors({
origin: "https://myapp.com", // only allow YOUR front-end
credentials: true // allow cookies to be sent
}));Note: Output:
(no visible output — it changes which origins the browser permits)
Now a browser will only let your own site at https://myapp.com call the API; a request from a random origin is blocked by the browser. credentials: true is needed if you send auth cookies along with requests. (Note: CORS is a browser rule, so it is one layer among several — not a complete shield, which is why we also add the others.)
Layer 4 — sanitize input against injection
An injection attack is when a user sneaks special characters into input hoping your code or database will execute them — for example MongoDB operators like $gt smuggled into a login form to trick a query, or <script> tags (an XSS attack — Cross-Site Scripting) meant to run in another user’s browser. Sanitizing strips or neutralises these before they do harm.
const mongoSanitize = require("express-mongo-sanitize");
app.use(mongoSanitize()); // strip $ and . from request data
// Always escape user content before showing it in HTML, e.g.
// "<script>" becomes "<script>" so the browser shows it, not runs it.Note: Output:
(no visible output — malicious keys are removed from req.body/req.query)
express-mongo-sanitize removes the $ and . characters that MongoDB query operators rely on, so an attacker cannot inject { "$gt": "" } to bypass a check. For XSS, the rule is: never trust user text in a page — escape it (or let your front-end framework escape it, as React does by default).
The hardening checklist
helmet()— secure HTTP headers on every response.express-rate-limit— cap requests per IP (stricter on/login).- Tight CORS — allow only your real front-end origin.
- Sanitize/validate all input (this plus the validation lesson).
- Keep secrets in
.env, run over HTTPS in production, and never leak raw error details to clients.
Tip: None of these is a silver bullet — security is layers. Each one closes a door; together they make your API a hard target. The best part is most are a single app.use(...) line, so there is no excuse to skip them.
Q. What does rate limiting protect against?
✍️ Practice
- Add
helmet()and a globalexpress-rate-limitto an API, then spam a route until you receive a 429. - Lock CORS to a single origin and confirm a request from a different origin is blocked.
🏠 Homework
- Harden your CRUD API with Helmet, a stricter rate limit on a
/loginroute, tight CORS, and input sanitization — then write a short note on what each layer defends against.