API Security Best Practices: How to Secure Your APIs Against Real-World Attacks

API Security Best Practices: How to Secure Your APIs Against Real-World Attacks
1 views

APIs are the number one attack surface for modern web applications. OWASP’s API Security Top 10 describes vulnerabilities that attackers exploit daily — broken authentication, excessive data exposure, mass assignment, and injection attacks. This guide covers each threat with the actual code-level defenses that prevent them, not just theory. This guide focuses on API security best practices, with the production trade-offs senior engineers need to make sound decisions.

⚡ TL;DR: Validate every input server-side. Use short-lived JWTs with refresh tokens. Implement rate limiting on every endpoint. Return minimum data (never expose internal fields). Use parameterized queries — never string concatenation. Set CORS to specific origins. Log auth failures. Never trust client-provided IDs for authorization.

OWASP API Top 10 — the real threats

// 1. Broken Object Level Authorization (BOLA/IDOR) — most common API flaw
// Attacker changes ID in URL to access other users' data

// VULNERABLE:
app.get('/invoices/:id', auth, async (req, res) => {
  const invoice = await db.getInvoice(req.params.id);
  res.json(invoice); // Returns invoice regardless of who owns it!
});
// Attacker: GET /invoices/12345 (not their invoice) → gets it!

// SECURE:
app.get('/invoices/:id', auth, async (req, res) => {
  const invoice = await db.query(
    'SELECT * FROM invoices WHERE id = $1 AND user_id = $2',
    [req.params.id, req.user.id] // ALWAYS scope to authenticated user
  );
  if (!invoice) return res.status(404).json({ error: 'Not found' });
  res.json(invoice);
});

// 2. Broken Authentication
// VULNERABLE: no token expiry, weak secret, algorithm=none
const token = jwt.sign(payload, 'weak');
// SECURE:
const token = jwt.sign(payload, process.env.JWT_SECRET, {
  expiresIn: '15m',    // Short-lived
  algorithm: 'HS256'   // Always specify algorithm explicitly
});
// ALWAYS verify: jwt.verify(token, secret, { algorithms: ['HS256'] })

Input validation and injection prevention

import { z } from 'zod';

// Validate EVERYTHING from the client — never trust request data
const CreateUserSchema = z.object({
  name: z.string().min(1).max(100).trim(),
  email: z.string().email().toLowerCase(),
  age: z.number().int().min(18).max(120),
  role: z.enum(['user', 'moderator']).default('user'),
  // NEVER accept: 'admin', isAdmin: true, userId, internalId, etc.
});

app.post('/users', async (req, res) => {
  const parsed = CreateUserSchema.safeParse(req.body);
  if (!parsed.success) return res.status(422).json({ error: parsed.error.format() });

  const { name, email, age, role } = parsed.data; // Only allowed fields
  await db.createUser({ name, email, age, role }); // Mass assignment prevented
});

// SQL injection prevention — ALWAYS parameterized queries
// VULNERABLE:
const user = await db.query(`SELECT * FROM users WHERE email = '${email}'`);
// Attacker: email = "x' OR 1=1 --" → returns all users!

// SECURE:
const user = await db.query('SELECT * FROM users WHERE email = $1', [email]);
// $1 is a parameter — never concatenated into SQL

Rate limiting and DDoS protection

import rateLimit from 'express-rate-limit';

// Different limits for different endpoints
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 10,                  // 10 attempts per IP per 15 min
  standardHeaders: true,    // Return rate limit headers
  message: { error: 'Too many attempts, try again in 15 minutes' },
});

const apiLimiter = rateLimit({
  windowMs: 60 * 1000, // 1 minute
  max: 100,            // 100 requests per minute
});

// Apply different limits to different routes
app.use('/auth/', authLimiter);  // Stricter: prevent brute force
app.use('/api/', apiLimiter);   // Standard: prevent abuse

// Also limit by API key (not just IP):
const keyLimiter = rateLimit({
  keyGenerator: (req) => req.headers['x-api-key'] || req.ip,
  max: 1000, // Per API key per minute
});

CORS and security headers

import cors from 'cors';
import helmet from 'helmet';

// WRONG: allow all origins
app.use(cors()); // Access-Control-Allow-Origin: *
// Anyone can call your API with a browser request from any domain!

// CORRECT: specific allowed origins
const allowedOrigins = [
  'https://myapp.com',
  'https://admin.myapp.com',
];

app.use(cors({
  origin: (origin, callback) => {
    if (!origin || allowedOrigins.includes(origin)) callback(null, true);
    else callback(new Error('CORS: Origin not allowed'));
  },
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
}));

// Helmet: sets secure HTTP headers automatically
app.use(helmet()); // Sets: X-Content-Type-Options, X-Frame-Options,
                   // Content-Security-Policy, Strict-Transport-Security, etc.

API security checklist

  • ✅ BOLA: always scope DB queries to authenticated user’s ID
  • ✅ Validate all inputs with schema validation (Zod, Joi, Yup)
  • ✅ Parameterized queries — never string-concatenated SQL
  • ✅ Short-lived JWTs (15m access + 7d refresh) with explicit algorithm
  • ✅ Rate limiting on auth endpoints (10 req/15min) and all API endpoints
  • ✅ CORS: specific origins, not wildcard
  • ✅ Helmet.js for security headers
  • ✅ Log auth failures, flag suspicious patterns
  • ❌ Never trust client-provided user IDs, roles, or permissions
  • ❌ Never return stack traces, SQL queries, or internal paths in errors

API security connects to the REST API design guide — security decisions should be made at design time, not retrofitted. The AWS S3 presigned URL guide shows how authentication works at the infrastructure layer. External reference: OWASP API Security Top 10.

Recommended Reading

→ Designing Data-Intensive Applications — The essential book every senior developer needs. Covers distributed systems, databases, and production architecture.

→ The Pragmatic Programmer — Timeless engineering wisdom for writing better, more maintainable code at any level.

Affiliate links. We earn a small commission at no extra cost to you.

Free Weekly Newsletter

🚀 Don’t Miss the Next Cheat Code

Join 1,000+ senior developers getting expert-level JS, Python, AWS, system design and AI secrets every week. Zero fluff, pure signal.

✓ No spam✓ Unsubscribe anytime✓ Expert-level only

Related: Node.js Security Best Practices: Protect Your API From the Most Common Attacks · Express.js Best Practices: Build Production-Ready APIs With Node.js · Day 68: Abuse and Cost Attacks

Related: HTTP Status Codes Explained: Every Code With Real-World Examples and When to Use Each · GraphQL vs REST: A Practical Comparison for Production APIs in 2025 · Docker Best Practices: Build Smaller, Faster, More Secure Images in Production

Deep-dive PDF

Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.

Already subscribed? or open the subscribe page.


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a comment

No account needed. Name and email are optional.