Secure API design is more than validating a bearer token. An API must determine whether a caller may access a specific object, modify a specific property, invoke a sensitive function, or consume a reasonable amount of system capacity. It must also remain predictable under malformed input, retries, traffic spikes, and changing client versions.
This guide explains practical API security best practices for authorization, authentication boundaries, schema design, resource limits, rate limiting, sensitive workflows, and the risks highlighted by the OWASP API Security Top 10.
Start With a Stable Security Boundary
An API is a contract between clients and services. That contract should expose business resources and operations rather than database tables, internal fields, or implementation details. Resource-oriented design uses noun-based paths such as:
GET /v1/customers/42/orders
GET /v1/orders/981
PATCH /v1/orders/981
POST /v1/ordersThis approach makes authorization decisions easier to reason about. For example, an order can have an owner, tenant, lifecycle state, and permitted transitions. A path such as /createOrder says little about the resource model and often encourages endpoint-specific security exceptions.
Keep public representations separate from persistence models. A database record may contain internal notes, fraud scores, audit fields, or administrative flags that should never be returned or accepted from a client. The resource-oriented API guidance provides a useful foundation for designing stable public resources.
Authentication Is Not Authorization
Authentication answers, “Who is making this request?” Authorization answers, “What may this identity do here?” Treating a valid access token as sufficient is one of the most common API security failures.

Establish a clear authentication boundary:
- Validate the token issuer, audience, signature, expiration, and relevant claims.
- Use the correct authentication mechanism for the client type, such as user sessions, OAuth access tokens, service credentials, or workload identity.
- Do not accept credentials in URLs, query strings, or logs.
- Keep authentication failures and authorization failures consistent without exposing unnecessary account information.
- Require stronger authentication or step-up verification for high-risk actions.
- Separate public, authenticated, administrative, and service-to-service entry points where their trust models differ.
Authentication middleware should establish an identity context, not make every business authorization decision. The application handling the request still needs to evaluate tenant membership, ownership, roles, resource state, and requested fields.
Prevent Broken Object-Level Authorization
Broken object-level authorization occurs when a caller can access another user’s or tenant’s object by changing an identifier. A request such as GET /orders/981 must not be authorized merely because the caller has a valid token.

Every object lookup should be scoped to the caller’s permitted security context:
order = orders.find_one( {
id: requested_id,
tenant_id: authenticated_user.tenant_id
} )
if not order:
return 404
if not policy.can_read(authenticated_user, order):
return 403Whether unauthorized resources return 404 Not Found or 403 Forbidden depends on the threat model and information-disclosure requirements. A 404 can avoid confirming that an object exists, while a 403 may be clearer for authenticated clients. Apply the choice consistently.
Do not rely on unpredictable identifiers as authorization. UUIDs and opaque IDs can reduce casual enumeration, but they are not a substitute for an authorization check. The same rule applies to nested resources, bulk endpoints, exports, file downloads, and asynchronous operation IDs.
Authorize Relationships, Not Just Roles
Role checks are often too coarse. A user may be an “editor” but only within one organization, project, or account. Effective policies commonly combine:
- Identity and tenant membership
- Role or permission
- Relationship to the resource, such as owner, assignee, or manager
- Resource state, such as draft, approved, or archived
- Requested action and sensitivity
- Network, device, or step-up authentication context where required
Centralize policy evaluation where possible, but keep the resource and action context explicit. Avoid scattered checks that become inconsistent as endpoints evolve.
Protect Properties Against Mass Assignment
Object authorization is only one layer. A caller may be allowed to update an object but not every field on it. Accepting request JSON directly into a database model creates a mass-assignment vulnerability.

For example, a user should not be able to submit this payload merely because the endpoint supports profile updates:
{
"display_name": "Alex",
"is_admin": true,
"account_status": "active",
"credit_limit": 100000
}Use separate input and output models, explicit allowlists, and field-level authorization. Document required, optional, immutable, input-only, and output-only fields. The field behavior guidance is useful when defining these rules.
For partial updates, define what omitted and null fields mean. A safe update flow should:
- Reject unknown fields instead of silently ignoring them.
- Reject immutable or output-only fields.
- Validate each field’s type, format, length, and allowed values.
- Apply authorization to sensitive fields independently.
- Use an explicit patch representation for partial changes.
PATCH /v1/users/42
Content-Type: application/json
{
"display_name": "Alex"
}Never infer that a missing field means “clear it” unless that behavior is explicitly documented. Ambiguous update semantics create both security and data-integrity problems.
Validate Inputs at the Contract Boundary
Validation should happen before business logic, database queries, external calls, or resource allocation. Use schemas to enforce:
- Primitive types and required fields
- String lengths and character constraints
- Numeric ranges and decimal precision
- Enum values and nullability
- Date, email, URI, and identifier formats
- Array lengths and nested-object depth
- Cross-field rules, such as valid date ranges
Reject malformed requests with a consistent status such as 400 Bad Request. Do not use validation as a substitute for authorization: a syntactically valid object ID can still belong to another tenant.
OpenAPI can define request and response schemas, support contract testing, and generate validation tooling. However, a schema cannot express every business rule, and generating a specification does not prove that the implementation follows it. Test authorization and abuse controls separately.
Use Safe, Machine-Readable Errors
Errors should help legitimate clients recover without revealing stack traces, SQL statements, access tokens, personal data, or internal topology. Use appropriate HTTP status classes: 4xx for client-side problems and 5xx for server or dependency failures.
The Problem Details standard provides a consistent format:
{
"type": "https://api.example.com/problems/validation-error",
"title": "Invalid request",
"status": 400,
"detail": "email is invalid",
"instance": "/requests/abc123"
}Keep detail safe for the caller and put sensitive diagnostic data in protected logs. Include a correlation or request identifier so support teams can investigate without exposing internal exception details.
Set Limits Before Attackers Set Them for You
Unrestricted resource consumption can cause denial of service, unexpected bills, queue saturation, or degraded service for other tenants. Define limits at the gateway and again at internal service boundaries so callers cannot bypass them.
Important limits include:
- Maximum request body and upload size
- Maximum decompressed payload size to address compression bombs
- Maximum number of fields, array items, and nesting depth
- Maximum page size and export size
- Query complexity, execution time, and database row limits
- Connection, concurrency, and queue limits
- Timeouts for downstream services and client requests
- Maximum redirect depth and permitted destinations for outbound requests
Collections should be paginated from the start. Use a documented default and maximum page size, and prefer opaque cursor or page-token pagination for large or changing datasets. An example is:
GET /v1/customers/42/orders?status=paid&limit=25&cursor=opaque-tokenDo not treat a page token as an authorization credential. Validate that it belongs to the appropriate query context, expires when appropriate, and cannot be modified to access another tenant. Stable ordering and a deterministic tie-breaker help prevent skipped or duplicated records.
Design Rate Limiting as Capacity Protection
Rate limiting should protect shared capacity while giving legitimate clients actionable feedback. A single global limit is rarely enough. Consider limits by:
- Authenticated principal or API key
- Tenant or subscription
- IP address, with care for shared networks
- Endpoint and operation cost
- Concurrent requests and outstanding jobs
- Resource type, such as login, search, export, or payment
Apply controls at public gateways and internal entry points. A compromised or misconfigured internal caller should not be able to bypass all protection by calling a service directly.
Return 429 Too Many Requests when a caller exceeds a quota, or 503 Service Unavailable when the service is temporarily overloaded. Include Retry-After when a useful retry time is known. Document whether quotas are per second, minute, day, or concurrent operation.
Rate limits should not become an information leak. Avoid revealing another user’s quota state, and make authentication endpoints resistant to credential stuffing without enabling trivial account enumeration.
Secure Sensitive Business Workflows
Some workflows are dangerous even when each individual request appears authorized. Examples include password resets, one-time-code verification, payment creation, account recovery, bulk exports, invitation sending, and coupon redemption.
Threat-model the entire workflow:
- Can the action be replayed?
- Can it be automated at high volume?
- Can a caller alter amount, recipient, tenant, or approval state?
- Does it require recent authentication or a second factor?
- Are attempts, failures, and completions auditable?
- Can parallel requests bypass a single-use rule?
Use server-side state transitions rather than trusting client-provided status values. Enforce approval separation where required, expire one-time tokens, and make sensitive actions idempotent or deduplicated.
Idempotency and Safe Retries
Clients may lose the response after the server has completed a request. Retrying a non-idempotent operation such as a payment or provisioning request can create duplicates. Accept an idempotency key for retryable writes and store the original result for a defined retention period.
POST /v1/payments
Idempotency-Key: 8f1-example-keyRepeated requests with the same key should return the original outcome when the request parameters match. Reject reuse with different parameters. Do not blindly retry validation failures, authorization failures, quota failures, or non-idempotent writes without such a mechanism. The automatic retry guidance offers useful principles for distinguishing transient failures from permanent ones.
For expensive operations, use an asynchronous operation resource:
POST /v1/exports
202 Accepted
Location: /v1/operations/op_123
{
"operation_id": "op_123",
"status_url": "/v1/operations/op_123"
}Protect operation status endpoints with the same object-level authorization as the original request. Polling should use backoff and have limits of its own.
Address Common OWASP API Risks
The OWASP API risks map directly to implementation decisions:
- Broken object-level authorization: scope every lookup and mutation to the caller’s permitted tenant and relationships.
- Broken authentication: validate credentials correctly, protect token issuance, and secure recovery and verification flows.
- Broken object-property authorization: use field allowlists and separate input/output models.
- Unrestricted resource consumption: impose payload, query, concurrency, timeout, pagination, and rate limits.
- Broken function-level authorization: protect administrative actions and hidden routes, not only ordinary resource reads.
- Unrestricted access to sensitive business flows: add replay prevention, workflow limits, step-up checks, and monitoring.
- Server-side request forgery: validate outbound URLs, restrict schemes and destinations, block private network ranges where appropriate, and avoid returning raw fetched content without controls.
- Security misconfiguration: remove debug endpoints, enforce TLS, configure CORS deliberately, and keep error responses safe.
- Improper inventory management: maintain an authoritative list of versions, routes, environments, owners, and deprecation dates.
- Unsafe consumption of APIs: validate third-party responses, set timeouts, limit payloads, and do not assume external data is trustworthy.
Use HTTP Semantics Consistently
Correct HTTP behavior improves both security and resilience. Use 201 Created for successful creation, normally with a Location header. Use 204 No Content when a successful operation has no response representation. Use 409 Conflict or 412 Precondition Failed for documented state or concurrency conflicts.
PUT generally represents replacement and is idempotent; PATCH represents partial modification and requires explicit rules. Use ETags or version fields to prevent lost updates when multiple clients edit the same object. A stale update should not silently overwrite a newer representation.
HTTP semantics, including method safety and idempotency, are defined in RFC 9110. Consistency also helps gateways, clients, caches, and observability systems distinguish successful requests from abuse and failure.
Version, Test, and Observe the Contract
Security controls must survive API evolution. Prefer additive changes such as optional fields and new endpoints. Treat changes to field meaning, enum behavior, defaults, error types, pagination ordering, and authentication claims as compatibility risks—not just changes to URL paths.
Maintain an inventory of every public and internal API version, owner, authentication model, data classification, and retirement date. A visible URI version such as /v2 is straightforward to document; header or media-type versioning keeps paths stable but can be harder to debug and cache.
Test negative behavior as deliberately as successful behavior:
- Read and modify another tenant’s object IDs.
- Submit unknown, immutable, output-only, and privileged fields.
- Use invalid, expired, malformed, or incorrectly scoped tokens.
- Send oversized bodies, deeply nested JSON, huge arrays, and expensive queries.
- Replay idempotency keys with identical and conflicting payloads.
- Abuse pagination tokens and operation identifiers.
- Trigger dependency timeouts, retries, queue saturation, and partial failures.
- Attempt SSRF through every URL-fetching feature.
Log structured outcomes without secrets or unnecessary personal data. Monitor authorization denials, validation failures, rate-limit events, unusual object enumeration, queue depth, latency, and dependency errors. Propagate a correlation or trace identifier so an event can be followed across gateways and services.
Pre-Release Security Checklist
- Are authentication and authorization enforced separately?
- Does every object access check tenant, ownership, relationship, and resource state?
- Are writable properties explicitly allowlisted?
- Are unknown fields, oversized payloads, expensive queries, and excessive nesting rejected?
- Do all collections have bounded pagination and safe continuation tokens?
- Are sensitive workflows protected against replay, automation, and race conditions?
- Are retryable writes idempotent?
- Do errors use safe, machine-readable responses without internal details?
- Are rate limits applied by meaningful dimensions and communicated with actionable responses?
- Are external calls protected by validation, timeouts, and response-size limits?
- Are API versions, routes, owners, deprecations, and authentication requirements documented?
- Do monitoring and alerting cover denials, abuse, resource exhaustion, and dependency failures?
Strong API security comes from enforcing multiple boundaries: identity, object, property, function, workflow, and resource consumption. When those boundaries are expressed in the contract, implemented in shared controls, and verified with negative tests and operational telemetry, the API becomes harder to abuse without becoming unpredictable for legitimate clients.
Author: Neeraj Mourya
Founder, Systems Architect – Digitobit
Specializing in scalable backend architecture and performance-focused SaaS systems

