Here’s a scary thought: every API you deploy is public unless you actively make it private.
Not “public” as in Google-indexed — public as in reachable, callable, and attackable.
Most backend breaches don’t come from elite hackers. They come from bored scripts, leaked tokens, or endpoints you forgot existed. Let’s walk through the most common API security mistakes I still see in production—and how to harden your backend without turning it into a usability nightmare.
1. “We Have Auth” Is Not a Security Strategy
Authentication answers who you are.
Authorization answers what you’re allowed to do.
Many APIs stop at auth.
GET /api/users/123
Authorization: Bearer <valid-token>
If the token is valid, the request succeeds — even if the user shouldn’t see that data.
The fix: enforce ownership and roles everywhere
// ❌ Only checks auth
if (!req.user) throw new UnauthorizedError();
// ✅ Checks authorization
if (req.user.id !== params.userId && !req.user.isAdmin) {
throw new ForbiddenError();
}
Security rule of thumb:
Every read, write, and delete must answer “why is this user allowed?”
2. JWTs Are Not Magic Shields
JWTs are great. Misused JWTs are dangerous.
Common issues:
- No expiration (
exp) - Tokens stored in
localStorage - Tokens accepted forever after user logout
- No audience (
aud) or issuer (iss) validation
Minimum safe JWT setup
const payload = jwt.verify(token, JWT_SECRET, {
audience: 'api.myapp.com',
issuer: 'auth.myapp.com',
});
Also:
- Short-lived access tokens (5–15 min)
- Refresh tokens stored httpOnly
- Token rotation on refresh
JWTs don’t make you secure — your validation logic does.
3. Rate Limiting Isn’t Optional Anymore
If your API has:
- Login
- OTP
- Password reset
- Search
- Public endpoints
…it needs rate limiting. Period.
Without it:
- Brute-force attacks are trivial
- Scrapers will eat your bandwidth
- One bad client can DOS your system
Simple rate limit example
import rateLimit from 'express-rate-limit';
export const limiter = rateLimit({
windowMs: 60 * 1000,
max: 100, // requests per minute
});
Apply different limits for:
- Auth endpoints
- Public APIs
- Internal services
Security isn’t about blocking users — it’s about controlling abuse.
4. Overexposed Data (aka Accidental Leaks)
This is the most common real-world breach I see.
Example:
{
"id": 42,
"email": "user@example.com",
"passwordHash": "...",
"isAdmin": false,
"createdAt": "..."
}
Nobody meant to expose passwordHash.
It just happened.
Fix: explicit response contracts
// ✅ Safe DTO
return {
id: user.id,
email: user.email,
createdAt: user.createdAt,
};
Never rely on:
- ORM default serialization
res.json(entity)- “We’ll filter it on the frontend”
If you didn’t whitelist it, it shouldn’t leave the server.
5. CORS Is Not an Auth Mechanism
I still hear:
“It’s safe, our CORS is locked down.”
CORS only affects browsers.
Attackers don’t use browsers.
curl https://api.yoursite.com/secret
CORS doesn’t stop:
- Server-to-server calls
- Bots
- Mobile apps
- Postman
- Curl
What CORS is for
- Preventing malicious websites from abusing your users’ browsers
What it’s not for
- Protecting your API
You still need auth, authorization, and rate limiting.
6. Secrets in the Wrong Places
If your repo ever contained:
-
.envfiles - API keys
- Firebase configs
- AWS credentials
…assume they’re compromised.
Rules that save careers:
- Secrets only in environment variables
- Rotate keys regularly
- Never log secrets (even in debug)
- Scope keys to the minimum permissions
If a key leaks, your blast radius should be tiny, not existential.
7. Missing Audit Trails
When something goes wrong, you need answers:
- Who did this?
- When?
- From where?
- Using which token?
If you don’t log security-relevant actions, you’re blind.
Log:
- Auth attempts
- Permission failures
- Admin actions
- Token refreshes
- Role changes
Not verbosely. Intentionally.
8. “Internal” APIs Are Still APIs
Microservices make this worse.
Just because an endpoint is:
- Behind a VPC
- On a private subnet
- “Only called by services”
…doesn’t mean it’s safe.
Protect internal APIs with:
- Service-to-service auth
- Short-lived tokens
- Network-level rules
- Explicit permissions
Zero trust isn’t paranoia — it’s realism.
Key Takeaway
API security isn’t one feature.
It’s a collection of boring, disciplined decisions:
- Explicit authorization
- Minimal data exposure
- Rate limits everywhere
- Short-lived credentials
- Clear audit logs
Most breaches don’t come from sophisticated exploits.
They come from defaults you forgot to change.




Top comments (1)
Strong write-up, very grounded.
I like how this focuses on boring defaults instead of hypothetical attackers... that’s where things actually go wrong.
Clear examples, practical fixes, no fear-mongering, just solid backend hygiene.
This is the kind of post people should read before shipping, not after an incident.