APIs are the front door to your data, and attackers know it: APIs are now the most common attack vector for web applications. Everything in this course so far, auth, validation, error design, feeds into security. This final lesson organizes it into a practical hardening checklist, aligned with the recurring themes of the OWASP API Security Top 10.
Every API endpoint must require TLS. Serve nothing meaningful over plain HTTP; redirect or reject it, and set HSTS (Strict-Transport-Security) so clients never downgrade. Tokens, API keys, and personal data on an unencrypted channel are public data. Internally, service-to-service traffic deserves TLS too; the network is not trustworthy just because you own it.
The most common real-world API vulnerability is Broken Object Level Authorization (BOLA): the server checks that a caller is logged in, but not that the object belongs to them. An attacker who owns invoice 1041 simply requests /invoices/1042.
// Vulnerable: any authenticated user can read any invoice
app.get('/invoices/:id', requireAuth, async (req, res) => {
res.json(await invoices.findById(req.params.id));
});
// Fixed: ownership is part of the query itself
app.get('/invoices/:id', requireAuth, async (req, res) => {
const inv = await invoices.findOne({ id: req.params.id, ownerId: req.user.sub });
if (!inv) return res.status(404).json({ error: 'not found' });
res.json(inv);
});Check authorization on every object access, and return 404 rather than 403 for objects outside the caller's scope so you do not confirm their existence. Using non-guessable IDs (UUIDs) helps, but is never a substitute for the check.
All injection defenses reduce to one principle: never let untrusted input become code. Use parameterized queries or an ORM so SQL injection is structurally impossible; never concatenate query strings. Validate every body, query, and path parameter against a schema, rejecting unknown fields, as covered in the previous lesson. That strict validation also blocks mass assignment attacks, where a caller adds "role": "admin" to a profile update and a careless handler persists it; explicitly allow-list the fields each endpoint may write.
Without limits, one script can brute-force logins, enumerate resources, or simply bankrupt your infrastructure. Rate limit per key or user (not just per IP), with tighter buckets on sensitive endpoints like login and password reset. Return 429 with Retry-After and expose X-RateLimit-* headers. Cap costs elsewhere too: maximum page sizes, maximum request body size (express.json({ limit: '100kb' })), and timeouts on all upstream calls.
Every leak compounds other attacks. Return generic 500 messages while logging details privately. Keep secrets in environment variables or a secrets manager, never in code or version control. Disable framework fingerprints like the X-Powered-By header, and use helmet in Express to set a battery of protective headers in one line. Configure CORS with an explicit allow-list of origins; a reflected wildcard with credentials is equivalent to no CORS at all. Finally, make error and 404 behavior uniform so attackers cannot map your internal structure from response differences.
Security is ongoing, not a launch checklist. Log authentication events, authorization denials, 4xx and 429 spikes, and alert on anomalies; a credential-stuffing attack looks like a spike of 401s. Keep dependencies patched (npm audit in CI), rotate credentials on a schedule, and test your API the way an attacker would: request other users' objects, replay expired tokens, send oversized and malformed bodies. Once a year, hire someone whose job is to break it.
Congratulations: you have completed the REST APIs course on codeyourcraft.com. Revisit Designing a Complete CRUD API and build one end to end with everything you now know.