The Security Debt That Destroys Companies
Most API security failures are not sophisticated attacks exploiting zero-day vulnerabilities. They are basic, well-documented attack vectors that developers leave unaddressed because security felt like something to worry about later. There is no later. If your API handles user data — and virtually every modern application does — security is a day-one requirement, not a post-launch concern.
Layer 1: Helmet — Secure Your HTTP Headers
The first line of defense is your HTTP response headers. The helmet middleware for Express configures these headers correctly in a single line:
const helmet = require('helmet');
app.use(helmet());
// Sets: Content-Security-Policy, X-Frame-Options, Strict-Transport-Security, and more
Layer 2: Input Validation — Trust Nothing From the Client
The most fundamental security principle in API development: every piece of data arriving from a client is potentially malicious. Validate everything against a strict schema before touching it. Use Zod for TypeScript projects and Joi for JavaScript projects:
const CreateUserSchema = z.object({
email: z.string().email().max(254),
password: z.string().min(8).max(72),
name: z.string().min(2).max(50).trim(),
});
const result = CreateUserSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ errors: result.error.issues });
}
Layer 3: JWT Authentication — Done Correctly
Never store JWTs in localStorage. localStorage is accessible from JavaScript, which means any XSS vulnerability in your application allows attackers to steal authentication tokens. Store access tokens in memory and refresh tokens in httpOnly secure cookies.
Use short-lived access tokens with refresh rotation. Access tokens should expire in 15-30 minutes. The refresh token is rotated on every use — this limits the damage window if a refresh token is compromised.
Layer 4: Rate Limiting — Prevent Abuse and Brute Force
Without rate limiting, your login endpoint is an invitation for brute force attacks. Use express-rate-limit to apply limits at different levels — a global limit for all requests and a strict limit for authentication endpoints (5 attempts per hour).
Layer 5: Centralized Error Handling — Never Expose Internal Details
Error messages are information for attackers. Your API should never return stack traces, database error messages, or internal paths in production responses. Use a centralized error handler that logs the full error internally while returning a sanitized response to the client.
Layer 6: Dependency Security — Your Supply Chain
Run npm audit as part of your CI/CD pipeline. Use dependabot (built into GitHub) to automatically open pull requests when your dependencies have known vulnerabilities. Outdated dependencies are one of the most common attack vectors in the wild.
Conclusion
API security is not a checklist you complete once — it is a discipline you maintain continuously. Implement these six layers from the beginning of your project. The cost of doing so is a few hours. The cost of a data breach — financially, reputationally, and legally — is orders of magnitude greater.
