Write down who you are defending against: the answer changes what you build. A public JSON API has four realistic attackers: a script crawling your routes for /api/users without an auth check and for id parameters it can increment; a logged-in customer reading another customer's order by changing one number in a URL; a malicious web page the victim visits, making authenticated requests with the victim's cookies; and a scraper.
Those map onto the OWASP API Security Top 10, whose 2023 edition is still current. Its first and third entries - Broken Object Level and Broken Object Property Level Authorization - are the two middleware cannot fix: only your handler knows whether u_77 may read order o_412. The rest is largely configuration.
| OWASP API 2023 risk | Concrete Express 24,430 failure | Fixed in |
|---|---|---|
| API1/API3 object and property authorization | findById(req.params.id) with no owner check | 3.6.10, 3.10.5 |
| API2 broken authentication | no expiry, no lockout, tokens in URLs | 3.6.5-3.6.7 |
| API4 unrestricted resource consumption | no rate limit, no body size cap | 3.10.7, 3.10.5 |
| API8 security misconfiguration | no security headers, CORS set to * | 3.10.2, 3.10.3 |
One principle ties the pipeline together: reject as early and as cheaply as possible. A request that fails a rate limit should never reach the body parser; a body over your size cap should never reach a validator; a payload that fails validation should never reach the database. Every stage you push a rejection earlier removes work an attacker can force you to do - exactly what a denial-of-service attempt is buying.
The second is less obvious: assume every defense will be misconfigured at least once. Test that the header is present, that a forged request gets a 403, that the eleventh request in a minute gets a 429 - four lines each in Supertest 14,405 (HTTP Testing with Supertest), and the only thing that notices when someone reorders app.use in a refactor.