← Back to Writeups
Nov 20, 2024·8 min read·SecurityAPIBackend

Building Secure APIs: A Practical Guide

APIs are the backbone of modern web applications, but they also represent a massive attack surface. Every endpoint you expose is a potential entry point for attackers. Building a secure API isn't about adding security as an afterthought — it needs to be woven into every design decision from the start.

Authentication Done Right

Use short-lived JWTs for stateless authentication and pair them with refresh tokens stored inHttpOnly cookies. Never embed sensitive data in the JWT payload — it's base64-encoded, not encrypted. Implement token rotation so that if a refresh token is compromised, it can only be used once before invalidation. For server-to-server communication, use API keys with strict IP allowlisting and rotate them on a regular schedule.

Rate Limiting and Throttling

Without rate limiting, your API is vulnerable to brute-force attacks, credential stuffing, and denial-of-service. Implement tiered rate limits: stricter limits on authentication endpoints (e.g., 5 requests per minute) and more relaxed limits on read-heavy routes. Use a sliding window algorithm backed by Redis for distributed environments. Always return a 429 Too Many Requests response with a Retry-After header so legitimate clients know when to retry.

Input Validation

Validate every piece of incoming data against a strict schema. Use libraries like Zod or Joi to define expected types, lengths, and patterns. Reject requests that don't conform rather than trying to sanitize them into shape. Pay special attention to nested objects and arrays — attackers often exploit deeply nested payloads to cause stack overflows or excessive memory consumption (known as "billion laughs"-style attacks).

Error Handling

Never expose stack traces, database errors, or internal paths in API responses. Use generic error messages for clients while logging detailed errors server-side. Differentiate between 4xx (client errors) and 5xx (server errors) consistently. A well-designed error response includes a machine-readable error code, a human-readable message, and a correlation ID for debugging — nothing more.

Defense in Depth

Layer your defenses: validate at the edge with a WAF, enforce authentication in middleware, check authorization at the route level, and validate input in the handler. Use CORS to restrict which origins can call your API. Enable HTTPS everywhere and set security headers likeStrict-Transport-Security and X-Content-Type-Options. Security is never a single layer — it's the sum of many small, deliberate choices.