API security best practices: beyond authentication

API security best practices: beyond authentication

11 min
securityapibackend

Securing APIs requires a multi-layered approach beyond just JWT tokens. We’ll cover rate limiting, input sanitization, OWASP top 10 for APIs, and security headers.


Markup Showcase: API Security

Headings and narrative

Security relies on layered controls: authentication, authorization, validation, monitoring, and response. Defense‑in‑depth reduces the blast radius when a single control fails.

Always validate inputs at trust boundaries. See the OWASP API Security Top 10 and internal security checklist for rollout steps. Avoid leaking sensitive data; mask values like Authorization: Bearer *** in logs.

Lists

  1. Authentication: short‑lived tokens, refresh flows, clock skew handling
  2. Authorization: least privilege, ABAC/RBAC, deny by default
  3. Validation: schema‑based (e.g., zod, ajv) at the edge and service
    • Normalize encodings
    • Enforce size limits
    • Reject unknown fields
  • Add rate limiter per IP + token
  • Enable anomaly detection on GraphQL operations

Caution: Security headers such as Content-Security-Policy and Strict-Transport-Security should be applied consistently across all routes.

Inline code

Return minimal error messages like 400 Bad Request with a machine‑readable body, but never echo untrusted input in the message.

Code blocks (TypeScript)

import { z } from "zod";

const Payload = z.object({
  email: z.string().email(),
  plan: z.enum(["free", "pro", "enterprise"]).default("free"),
});

export async function handler(req: Request) {
  const ip = req.headers.get("x-forwarded-for") ?? "unknown";
  // Simple rate limit interface (pseudo)
  const allowed = await rateLimit.allow({
    key: `signup:${ip}`,
    limit: 20,
    window: 60_000,
  });
  if (!allowed) return new Response("Too Many Requests", { status: 429 });

  const json = await req.json();
  const parsed = Payload.safeParse(json);
  if (!parsed.success) {
    return new Response(JSON.stringify({ error: "invalid_payload" }), {
      status: 400,
      headers: { "content-type": "application/json" },
    });
  }

  // ... business logic
  return new Response(JSON.stringify({ ok: true }), { status: 200 });
}

JSON example

{
  "security": {
    "rateLimit": { "windowMs": 60000, "max": 100 },
    "cors": { "origins": ["https://example.com"], "allowCredentials": false },
    "headers": [
      "Strict-Transport-Security: max-age=31536000; includeSubDomains",
      "X-Content-Type-Options: nosniff"
    ]
  }
}

HTML snippet

<meta
  http-equiv="Content-Security-Policy"
  content="default-src 'self'; img-src https: data:; object-src 'none'"
/>

Table

ControlPurposeExample
Rate limitingThrottle abusetoken+IP sliding window
Input validationPrevent injectionJSON schema at gateway
Content SecurityReduce XSS vectorsCSP with nonces

Long URL wrapping test

https://example.com/security/this/is/a/very/long/path/that/should/wrap/or/scroll/properly/depending/on/styles/applied/to/links


Image

Lock on metal door