Skip to content
Back to blog

API Security Best Practices for Production APIs

July 7, 202613 min read

Most API security checklists stop at "use HTTPS and check the JWT." That covers the front door, but real API breaches rarely happen there — they happen in the gaps between the front door and everything behind it: an authenticated user reading someone else's order by changing an ID in the URL, an internal service call that nobody rate-limited, a partner integration that turned into a path to your internal metadata endpoint.

This guide walks through API security as six connected layers rather than a flat list: identity and access, minimizing what's exposed, protecting secrets and input, controlling traffic, containing third-party risk, and staying able to see what's actually happening in production. Each layer closes a specific class of real-world attack, and skipping one doesn't just weaken it — it usually undermines the layers around it too.

Identity & Access1OAuth, MFA, BOLA checksLeast Exposure2Scopes, data, encryptionSecrets & Input3Vault, schema validationTraffic Control4Rate limits, abuse defenseThird-Party Risk5Egress gate, hardeningVisibility6Inventory, logs, alerts
API security: six areas, twelve practices

Identity and Fine-Grained Authorization

Modern OAuth 2.0/OIDC with PKCE and short-lived tokens (a 15-minute access token, not a token that lives for a week) has become the baseline for API authentication, and MFA at login closes the most common account-takeover path: a leaked password alone. But authentication only answers "who is this," not "what can they touch" — and that second question is where most real API breaches actually happen.

The attack that shows up over and over in breach reports is BOLA — Broken Object Level Authorization. A user is fully authenticated, holds a valid token, and simply changes an ID in the request (`/orders/42` to `/orders/99`) to read or modify someone else's data. The API checked *who* made the request but never checked whether that user was allowed to touch *that specific object*. This is consistently the top item on the OWASP API Security Top 10 for a reason: it's invisible in code review unless someone is specifically looking for it, and it doesn't require breaking encryption or stealing a token — just changing a number.

Closing this gap means checking authorization at three levels on every request, not just at login: object-level (does this user own or have access to this specific record), function-level (is this role even allowed to call this action, regardless of which object), and field-level (which fields in the response should this caller actually see). A support agent role might legitimately call the same endpoint as a customer, but should never see the same fields.

1Object checkDoes this user own this record?2Function checkCan this role call this action?3Field checkWhich fields can they see?
Authorization checks on every request, not just at login

Quick reference

  • Use OAuth 2.0/OIDC with PKCE and short-lived access tokens; pair login with MFA to stop credential-stuffing.
  • BOLA (Broken Object Level Authorization) is consistently the #1 API vulnerability in industry breach data — check for it deliberately.
  • Authorization is three checks, not one: object ownership, function/role permission, and field-level visibility.
  • Run these checks on every request, not just once at session start — tokens don't expire mid-session, but permissions can.
  • Log denied authorization attempts distinctly from denied authentication attempts — they indicate different attack patterns.
  • Test authorization with a second user account, not just a second role — role-based tests miss object-level bugs entirely.

Remember this

A valid token proves who someone is, not what they're allowed to touch — check object, function, and field-level access on every request, because BOLA is the vulnerability that authentication alone will never catch.

Minimize Exposure: Scopes, Data, and Transport

Least privilege applies to both what a client can *do* and what data it *gets back*. It's common for an API to return an entire internal record — salary, internal IDs, role flags — because it was convenient to serialize the whole database object, even though the caller only needed a name and an ID. Every extra field returned is a field that can leak through a misconfigured client, a logging pipeline, or a browser extension with too much access. Scope tokens narrowly (a mobile app reading order status doesn't need a scope that can also cancel orders), and filter response payloads down to exactly what the caller needs, not what happened to be on the object.

Encryption in transit is the other half of minimizing exposure, and it's easy to think of as "done" once TLS terminates at the edge — but that's often where real coverage stops. Traffic between your API gateway and internal services frequently runs unencrypted on the assumption that the internal network is trusted, which is exactly the assumption that turns a single compromised internal host into full lateral access. Mutual TLS (mTLS) between internal services means each side authenticates the other with a certificate, not just the connection being encrypted — closing the gap between "encrypted" and "actually mutually authenticated."

Quick reference

  • Scope tokens to the minimum action set a client needs — read-only scopes should not be able to write or delete.
  • Filter API responses to the fields the caller needs; don't serialize an entire internal record by default.
  • TLS termination at the gateway is not the same as encryption end-to-end — check what happens after the gateway.
  • Use mTLS between internal services once you have more than a couple of services — the internal network is not automatically trusted.
  • Rotate TLS certificates automatically; a manually-tracked certificate expiry is an outage waiting to happen.
  • Treat over-exposed fields as a bug class, not a one-off review comment — add automated response-schema checks in CI.

Remember this

Least privilege applies to data as much as actions — scope tokens narrowly, filter responses to what's needed, and don't let 'TLS at the edge' quietly stand in for encryption all the way through.

Protect Secrets and Validate Every Input

Signing keys and client secrets are the material an attacker needs to forge valid tokens or impersonate a trusted client — which makes them a higher-value target than almost any single API endpoint. Storing them in a secrets manager backed by an HSM (hardware security module), rather than in environment variables or config files checked into a repository, means the raw key material never needs to be visible to a human or copy-pasted between systems. The unglamorous but critical part is rotation: a secret that's never rotated is a secret that, once leaked (via a misconfigured CI log, a debug endpoint, an old backup), stays valid indefinitely.

On the input side, schema validation at the edge — before a request reaches business logic — rejects malformed input cheaply instead of letting it flow deeper into the system. A request with an unexpected field, an oversized payload, or a malformed URL should get a fast, generic 400 response at the perimeter, not a stack trace three layers into your service after it's already touched a database query. This is the same principle as a bouncer checking ID at the door instead of security discovering an underage patron at the bar: reject early, reject cheaply, and don't let untrusted input reach code that assumes it's already valid.

Quick reference

  • Store signing keys and client secrets in an HSM-backed secrets manager (Vault, AWS KMS, etc.) — never in env files or repos.
  • Rotate secrets on a schedule, not only after a suspected leak — a never-rotated secret has an unbounded blast radius.
  • Validate request schemas at the edge: reject unknown fields, oversized payloads, and malformed types before business logic runs.
  • A rejected malformed request should return a generic 400, not an error that reveals internal structure or a stack trace.
  • Set explicit payload size limits — an unbounded request body is a resource-exhaustion vector, not just a validation nuisance.
  • Audit who and what can read secrets from the vault, not just who can write them — read access is the actual attack surface.

Remember this

Secrets need rotation, not just storage — and input validation belongs at the edge of the system, rejecting bad requests before they reach code that assumes the input is already trustworthy.

Control Traffic and Defend Sensitive Flows

Rate limiting, payload caps, and request timeouts exist to stop one client — malicious or simply buggy — from degrading the API for everyone else. A limit of 100 requests per minute, a 1MB request cap, and a 30-second timeout aren't arbitrary numbers; they're a statement about what normal usage looks like, with anything beyond that treated as abuse until proven otherwise. These limits should be enforced at the gateway, before the request reaches application code that has to spend real resources processing it.

Generic rate limiting isn't enough for the flows that attackers specifically target: login, checkout, signup, and OTP verification. These "sensitive business flows" need their own defense layer, because a rate limit generous enough for normal checkout traffic is also generous enough for a credential-stuffing bot working through a leaked password list. Velocity rules (flagging unusually rapid attempts from one identity or IP), idempotency keys (preventing a retried checkout from double-charging a customer), CAPTCHA, and step-up MFA (requiring a second factor specifically when a flow looks risky, not on every login) work together to make these flows expensive to automate at scale without adding friction to the vast majority of legitimate users.

Quick reference

  • Enforce rate limits, payload size caps, and timeouts at the gateway — before requests consume application resources.
  • Generic rate limits protect availability; sensitive flows (login, checkout, signup, OTP) need dedicated abuse defenses.
  • Velocity rules catch credential stuffing and OTP-guessing bots that stay just under a naive per-minute rate limit.
  • Idempotency keys on checkout and payment endpoints prevent retries (network glitches, double-clicks) from double-charging.
  • Step-up MFA should trigger on risk signals (new device, unusual location), not add friction to every single login.
  • CAPTCHA is a speed bump, not a wall — pair it with velocity rules and monitoring rather than relying on it alone.

Remember this

Generic rate limits protect uptime; the flows attackers actually target — login, checkout, signup, OTP — need their own layer of velocity rules, idempotency keys, and step-up checks.

Contain Third-Party Risk and Harden Defaults

Security conversations focus heavily on inbound requests, but outbound calls your own API makes — to partner APIs, webhooks, or internal services — are an attack surface too. Server-Side Request Forgery (SSRF) is the attack that exploits this: if your API accepts a URL from user input and fetches it server-side without restriction, an attacker can point that fetch at your cloud provider's internal metadata endpoint (`169.254.x.x`) and potentially extract credentials your own server has access to. An egress gate that allowlists approved destinations, blocks internal/link-local IP ranges by default, and validates the shape of partner responses closes this path without requiring you to trust every URL a user or partner might supply.

Hardening configuration and error handling closes the gaps that don't require a sophisticated attack at all — just a default that was never locked down. "Deny by default" means explicitly allowing only the HTTP methods, origins, and routes a service actually needs, rather than allowing everything and trying to remember to block the dangerous parts. Strict CORS policies, explicitly locked-down allowed methods, and debug mode forced off in production prevent an API from handing an attacker a detailed stack trace, an open OPTIONS response, or a debug endpoint that was only ever meant for local development.

1Outbound callYour API calls another service2Allowlist checkIs this destination approved?3Validate responseReject unexpected shapes
Outbound calls get checked too, not just inbound requests

Quick reference

  • Treat outbound calls as an attack surface: an egress gate should allowlist destinations, not just trust any URL supplied.
  • Block requests to internal and link-local IP ranges (169.254.x.x and similar) by default to prevent SSRF into cloud metadata.
  • Validate the shape and size of partner API responses — a compromised or misbehaving partner shouldn't crash your service.
  • Default to deny: explicitly allow only the HTTP methods, origins, and routes a service actually needs.
  • Lock CORS down to known origins in production; a wildcard CORS policy is a common, easily-missed misconfiguration.
  • Force debug mode and verbose error output off in production — a stack trace is a map of your internals handed to an attacker.

Remember this

Outbound calls need the same scrutiny as inbound ones — an egress allowlist stops SSRF, and 'deny by default' configuration stops the breaches that never needed a sophisticated attacker at all.

Inventory, Log, Detect, and Respond

You can't secure an API you don't know exists. Shadow APIs — endpoints deployed without going through the same review, documentation, or security process as the rest of the system — and forgotten legacy versions (a `/v1/` route everyone assumes is dead but is still reachable) are consistently where real incidents originate, precisely because nobody was watching them. An API registry that tracks every API, its version, and its status (active, deprecated, sunset) turns "we didn't know that was still running" from a plausible excuse into something that shows up on a dashboard before it becomes an incident.

The last layer is detection and response, because prevention will eventually fail somewhere — the goal is making sure failure is noticed in minutes, not discovered months later in a breach report. Audit logs should capture authentication and authorization decisions, admin actions, and configuration changes specifically (not just generic request logs), because those are the events that matter most during an investigation. Feeding those logs into a SIEM with real-time alerting on anomalies — a spike in 401 responses, an unusual pattern of admin actions — is what turns logging from a compliance checkbox into something that actually catches an attack while it's happening rather than after the damage is already done.

Quick reference

  • Maintain a live API registry: every API, its version, and its status (active, deprecated, sunset, shadow).
  • Shadow APIs — deployed without going through standard review — are a recurring root cause in real incident reports.
  • Audit logs should specifically capture auth decisions, admin actions, and config changes, not just generic access logs.
  • Feed logs into a SIEM with real-time alerting; a log nobody reads until after an incident isn't providing detection.
  • Alert on anomalies specifically (spikes in 401s, unusual admin activity), not just raw traffic volume.
  • Decommission old API versions deliberately — a route people assume is dead but is still reachable is a live attack surface.

Remember this

You can't secure what you can't see — keep a live inventory of every API and version, and make sure audit logs and alerts turn an eventual failure into a minutes-long detection instead of a months-long surprise.

Key takeaway

Share:

None of these six layers is optional in isolation — strong authentication with no object-level authorization checks still lets an attacker read someone else's data; strict rate limits with no egress controls still let SSRF through the back door; perfect encryption with no logging still leaves you finding out about a breach from a customer instead of a dashboard. The layers work because they cover different failure modes, and a checklist approached one panel at a time misses how they reinforce each other.

If you're prioritizing where to start: fix object-level authorization first (it's the most common real-world breach and the easiest to test for with a second user account), then close the egress/SSRF gap, then build the audit logging that lets you actually notice when something goes wrong. Everything else on this list matters, but those three closes the gaps that attackers exploit most often in practice — the rest is defense in depth on top of that foundation.

Related Articles

Every REST API needs a way to verify who is calling it and what they are allowed to do. The method you choose shapes you

Read

Authentication in modern apps spans three distinct models. Sessions store user state on the server and send a session ID

Read

Many .NET teams stop at **"add [Authorize] and check roles."** That covers two of seven authorization models ASP.NET Cor

Read

Explore this topic

Keep learning

Follow a structured path or browse all courses to go deeper.