Most Node.js APIs I've been asked to audit or take over weren't broken into through anything exotic. It was almost always one of a handful of ordinary gaps — a missing check, a default left on, a header nobody thought about. Here's the checklist I actually run through, in the order I check it.
Authentication Isn't Optional Middleware
Every route that touches user data or performs an action needs to verify who's calling it, and that check needs to happen before any business logic runs — not as an afterthought bolted onto a controller. I apply auth middleware at the router level, grouped by route, so there's no path where a new endpoint can accidentally ship without it. A missing auth check on one forgotten route is still a security hole, even if every other route is locked down correctly.
Validate Input at the Boundary, Not in Your Business Logic
Every field coming in from a request body, query string, or URL param is untrusted until proven otherwise. I validate shape, type, and constraints (using something like Zod or Joi) at the very edge of the request, before that data touches a database query or gets passed to another service. This does two things at once: it blocks malformed or malicious input immediately, and it keeps your actual business logic from having to defensively re-check things it should be able to trust.
Input validation isn't just about security — it's what stops a typo in a request body from becoming a confusing 500 error three layers deep in your code, instead of a clean 400 at the door.
Rate Limit Before Someone Else Finds Out You Didn't
An unthrottled API is an open invitation for brute-force login attempts, scraping, and simple denial-of-service by accident or on purpose. A rate limiter on every route, with tighter limits on auth endpoints specifically, is cheap to add and closes off an entire category of abuse. I set it up early rather than after the first incident.
Never Let Secrets Reach a Public Repository
API keys, database credentials, and JWT signing secrets belong in environment variables, loaded from a .env file that's git-ignored from the very first commit — not added to .gitignore after the fact once someone notices it's tracked. I also rotate any secret that was ever accidentally committed, even briefly; removing it from the latest commit doesn't remove it from git history.
CORS Isn't a Formality
A CORS policy tells browsers which origins are allowed to call your API directly from client-side JavaScript. It's not a substitute for authentication, but a badly configured one still widens your attack surface unnecessarily.
A Wildcard CORS Policy Is a Security Bug
Access-Control-Allow-Origin: *combined with credentials enabled is a real misconfiguration, not just a lint warning- List the specific origins that should be allowed to call the API, and nothing else
- Re-check this list whenever a new frontend or partner integration is added — it's easy to forget it exists
Log Enough to Investigate, Not Enough to Leak
When something does go wrong, you need enough logging to reconstruct what happened — request paths, status codes, timestamps, user IDs. What you don't want in those logs is passwords, tokens, or full request bodies containing personal data. I explicitly strip sensitive fields before anything gets logged, so the logging system itself never becomes the thing that leaks the data you were trying to protect.
Takeaway
None of this requires exotic security tooling — auth on every route, validated input at the boundary, rate limiting, secrets kept out of source control, a real CORS policy, and logs that don't leak. Most Node.js APIs that get compromised are missing one of these basics, not falling to some sophisticated attack.