Call
APIs

REST API Best Practices: HTTP Semantics, Idempotency, Errors, Pagination, Caching, and Security

Build REST APIs around real HTTP semantics: resource-oriented URLs, correct methods and status codes, idempotency, Problem Details errors, cursor pagination, caching and ETags, validation, authorization, rate limits, OpenAPI contracts, and safe observability.

Published: November 6, 2025
Updated: September 12, 2026
13 min read
REST API Best Practices: HTTP Semantics, Idempotency, Errors, Pagination, Caching, and Security

REST API Best Practices: HTTP Semantics, Idempotency, Errors, Pagination, Caching, and Security

A good REST API is not “JSON over HTTP with nice URLs.” It uses HTTP semantics deliberately so clients, proxies, caches, SDKs, gateways, and operators can understand what a request means without private conventions everywhere.

The most useful REST practices are not style debates about singular vs plural nouns. They are correctness rules around resource identity, methods, idempotency, status codes, validation, errors, caching, pagination, authorization, and compatibility.

Model resources, not database tables

A public API resource should represent a product/domain concept:

/orders/o_42
/customers/c_7
/invoices/i_900

It does not need to mirror one SQL table exactly.

For example, an Order response may combine:

  • relational data
  • payment state
  • shipping state
  • derived totals

Keep storage implementation behind the API boundary so database refactors do not force unnecessary client migrations.

Use stable resource identifiers

Once a public resource URI is published, clients may store it in logs, workflows, links, and integrations.

Prefer stable opaque identifiers over mutable human labels:

/orders/o_42

instead of:

/orders/asha-singh-2026-september-keyboard

when the latter can change.

Readable slugs are fine where they are the actual durable identifier; do not make mutable presentation data the only identity by accident.

Let HTTP methods carry their standard meaning

RFC 9110 defines semantics for methods such as GET, POST, PUT, DELETE, and others.

A practical mapping is:

GET

Retrieve a current representation without requesting a state change.

GET /orders/o_42

POST

Submit data for resource-specific processing; often used to create a new server-assigned resource.

POST /orders

PUT

Create/replace state at a known target URI using idempotent semantics.

PUT /users/u_42/preferences

PATCH

Apply a partial modification using a documented patch format/semantics.

PATCH /orders/o_42

DELETE

Request removal of the target resource or its current association/state according to your contract.

Do not make every operation POST /doSomething when HTTP already has useful method semantics.

Safe and idempotent are different properties

RFC 9110 calls a method safe when the client does not request a state change.

GET and HEAD are safe.

An idempotent method has the same intended server effect when the same request is repeated.

HTTP defines:

  • safe methods as idempotent
  • PUT as idempotent
  • DELETE as idempotent

POST is not inherently idempotent.

That distinction matters when networks fail and clients retry.

Idempotency is essential for important POST operations

Consider payment creation:

client sends POST /payments
server charges card
response is lost
client retries

Without protection, the user may be charged twice.

For non-idempotent business operations that may be retried, accept a client-generated idempotency key:

Idempotency-Key: checkout-7f8c...

Store the logical result under a scope that includes the correct caller/tenant and return the same outcome for a valid duplicate attempt.

Idempotency keys need:

  • uniqueness scope
  • retention duration
  • request-payload mismatch handling
  • concurrency safety

Do not treat an arbitrary key header as idempotency unless the server actually enforces it atomically.

Use status codes for transport-level semantics

Clients should not need to parse a JSON string to discover whether authentication failed.

Use HTTP status codes intentionally.

Common examples:

200 OK                successful retrieval/update response
201 Created           new resource created
202 Accepted          asynchronous work accepted, not completed
204 No Content        success with no response representation
400 Bad Request       malformed/invalid generic request
401 Unauthorized      authentication required/invalid
403 Forbidden         authenticated but not permitted
404 Not Found         resource unavailable/not exposed
409 Conflict          state conflict
412 Precondition Failed conditional request failed
415 Unsupported Media Type
422 Unprocessable Content semantically invalid content where appropriate
429 Too Many Requests
500 Internal Server Error
503 Service Unavailable

Choose based on RFC semantics and your contract, not arbitrary internal numeric mappings.

201 Created should identify the new resource

When a POST creates a resource, return its identifier/representation and usually a Location header where appropriate:

HTTP/1.1 201 Created
Location: /orders/o_42
Content-Type: application/json

Body:

{
  "id": "o_42",
  "status": "pending"
}

This gives the client canonical server state without requiring an immediate second lookup.

Use 202 Accepted only for truly asynchronous completion

202 means the request was accepted for processing, not that the business operation completed successfully.

A useful asynchronous API provides a status resource:

POST /exports

returns:

{
  "job_id": "job_42",
  "status_url": "/export-jobs/job_42"
}

Then clients can poll or subscribe to progress.

Do not return 202 for a synchronous database write simply because the server may do optional background work afterward.

Standardize error bodies

RFC 9457 defines Problem Details for HTTP APIs using application/problem+json.

A response can look like:

{
  "type": "https://api.example.com/problems/insufficient-stock",
  "title": "Insufficient stock",
  "status": 409,
  "detail": "Only 1 unit remains",
  "instance": "/orders/attempts/a_900"
}

You can add carefully documented extension members such as:

{
  "error_code": "INSUFFICIENT_STOCK",
  "available": 1
}

This is better than inventing a different error envelope for every endpoint.

Separate machine-readable codes from human messages

Clients should branch on a stable machine-readable code/type, not exact English text.

Bad:

if message == "Card declined": ...

Better:

problem.type / error_code → stable client behavior
problem.detail → human/debug context

Human-readable text can change, be localized, or contain contextual details.

Do not leak internal exceptions

OWASP recommends avoiding unnecessary technical details in REST error responses.

Do not return:

  • stack traces
  • SQL text
  • filesystem paths
  • internal hostnames
  • secret values
  • downstream tokens

Return a safe public problem response and log diagnostic detail internally with a trace/request ID.

Validate request shape and business rules separately

There are several validation layers.

Transport/content validation

  • allowed Content-Type
  • body size
  • JSON syntax

Schema validation

  • required fields
  • types
  • string length
  • enum values
  • numeric range

Business validation

  • enough inventory
  • user may modify this order
  • transition paid → pending not allowed

Keeping these distinctions clear produces better errors and safer code.

Reject unsupported media types explicitly

If an endpoint only accepts JSON:

Content-Type: application/json

then reject unexpected content types with appropriate HTTP semantics such as 415 Unsupported Media Type.

Do not feed arbitrary bodies into whichever parser happens to accept them.

Set request-body size limits before parsing large attacker-controlled payloads.

Authorization must be resource-specific

Authentication says who the caller is.

Authorization asks whether that caller may access this resource and action.

For:

GET /tenants/t7/invoices/i42

verify:

  • authenticated identity
  • membership/access to tenant t7
  • access to invoice i42
  • permission for the requested action

Do not trust tenant/user IDs supplied by the client merely because the token is valid.

OWASP recommends access control at each non-public REST endpoint.

Avoid object-level authorization bugs

One of the most common API failure patterns is:

user can access invoice 10
changes URL to invoice 11
server checks authentication only
returns someone else's invoice

Resource lookup should be scoped through authorization context:

find invoice i11 that belongs to tenant allowed for caller

rather than fetching arbitrary object first and hoping every handler remembers an access check later.

Use cursor pagination for large mutable collections

Offset pagination is simple:

/orders?limit=50&offset=5000

It is fine for small/admin datasets.

For large frequently changing feeds, cursor/keyset pagination is often better:

/orders?limit=50&after=opaque_cursor

The cursor should encode or reference a stable indexed ordering such as:

created_at + unique ID

Keep cursors opaque so clients do not depend on internal database details.

Put a hard maximum on page size

A client request such as:

?limit=1000000

should not become an accidental export endpoint.

Define:

  • default page size
  • maximum page size
  • stable ordering
  • next cursor/link behavior

For true bulk export, provide a separate asynchronous export workflow optimized for that use case.

Return navigation metadata that clients can use

A cursor page might return:

{
  "data": [ ... ],
  "page": {
    "next_cursor": "opaque...",
    "has_more": true
  }
}

or links:

next
prev

Do not force clients to reverse-engineer whether another page exists from “array length happened to equal 50.”

Filtering and sorting should be explicit

Use documented query parameters:

/orders?status=paid&sort=-created_at

Avoid exposing raw SQL-like filter expressions unless you intentionally want to support a query language and can secure/cost-control it.

Whitelist sortable/filterable fields.

An arbitrary sort=huge_unindexed_expression should not let clients turn the API into a database stress tool.

Use HTTP caching where data semantics allow it

REST resource URLs work naturally with HTTP caching.

A public resource can return:

Cache-Control: public, max-age=60

Private user-specific data might use:

Cache-Control: private, no-cache

or no-store when storage must be prohibited.

Do not mark personalized responses public simply to get CDN hits.

Caching policy is part of the security boundary.

Use validators to avoid retransmitting unchanged data

A server can return an ETag:

ETag: "order-o42-v17"

The client later sends:

If-None-Match: "order-o42-v17"

If unchanged, the server can return:

304 Not Modified

This preserves freshness checks while reducing body transfer.

Last-Modified / If-Modified-Since are another validator pair when timestamps represent the resource accurately.

Use conditional writes to prevent lost updates

ETags are useful for concurrency too.

Client reads:

ETag: "order-o42-v17"

Then updates with:

If-Match: "order-o42-v17"

If someone changed the order meanwhile, the precondition fails instead of silently overwriting the newer version.

This is an HTTP-native optimistic concurrency pattern.

Rate limit by meaningful identity

Possible rate-limit dimensions include:

  • user
  • API key
  • OAuth client
  • tenant
  • IP as one signal
  • endpoint/cost class

Do not rely only on IP for authenticated multi-tenant APIs; NATs and proxies can put many users behind one address.

Return 429 Too Many Requests when policy is exceeded and include documented retry information if your API supports it.

Rate limiting should protect both infrastructure and fair-use/product quotas.

Different endpoints may need different cost limits

One cheap lookup and one expensive report should not necessarily share the same request quota.

Model endpoint weights or separate buckets:

GET /countries      cheap
POST /search        medium
POST /large-export  asynchronous / strict quota

Rate limiting only by raw request count can make cheap traffic unusable or expensive traffic under-protected.

Document the API as a contract

OpenAPI provides a language-agnostic description format for HTTP APIs.

A good OpenAPI document can drive:

  • reference documentation
  • client/server code generation
  • contract tests
  • mocks
  • security review
  • linting

Keep the specification generated from or checked against actual server behavior.

A beautiful OpenAPI file describing an endpoint the server no longer implements is worse than no contract because consumers trust it.

Do not confuse OpenAPI version with your API version

An OpenAPI document may say:

openapi: 3.1.1

That describes the OpenAPI Specification version used by the document.

Your business API may still be:

API v1

These version numbers solve different problems.

Design retries from method semantics and business semantics

RFC 9110 defines safe/idempotent method behavior so clients/intermediaries know when retries can be reasonable.

But the application still matters.

Examples:

  • GET /products/42 is normally safe to retry
  • a correctly designed PUT replacement is idempotent
  • POST /payments is not automatically safe

Use timeouts, bounded exponential backoff, jitter, and idempotency keys for suitable operations.

Never configure a gateway to blindly retry every POST after a timeout.

Distinguish request timeout from business completion

A client timing out does not prove the server did nothing.

If the connection breaks after a write committed, the result may be ambiguous.

For important mutations:

  • use idempotency keys
  • expose operation/resource status
  • let clients safely query the canonical result

This is a business protocol problem, not just an HTTP library setting.

Handle asynchronous operations as resources

For long-running work:

POST /video-exports

return a job resource:

{
  "id": "job_42",
  "status": "queued"
}

Clients can retrieve:

GET /video-exports/job_42

and eventually see:

queued → working → completed/failed

This is easier to reason about than holding an HTTP request open for several minutes without need.

Keep webhooks and callbacks idempotent too

If your API sends webhooks, receivers can experience duplicates due to retries and ambiguous network failures.

Give each delivery/event a stable ID and sign webhook requests.

Document:

  • retry schedule
  • signature verification
  • event ordering guarantees
  • duplicate semantics
  • retention/replay availability

A REST API ecosystem often includes outbound APIs as well as inbound endpoints.

Use HTTPS everywhere for protected APIs

OWASP's REST Security guidance recommends HTTPS-only endpoints for secure REST services.

TLS protects:

  • bearer tokens
  • API keys
  • cookies
  • request/response bodies

Do not send credentials in query parameters when they can end up in logs/history.

Use headers/cookies/appropriate authentication mechanisms with least-privilege scopes.

CORS is a browser permission boundary, not API authentication

CORS tells browsers which origins may read responses from frontend JavaScript.

It does not stop:

  • curl
  • backend services
  • attackers sending direct HTTP requests

Configure CORS narrowly for browser clients, but keep authentication/authorization independent of it.

Do not use Access-Control-Allow-Origin: * with a credentialed private API design that requires restricted origins.

Version only when compatibility truly breaks

Avoid creating /v2 for every additive response field.

Prefer backward-compatible evolution where possible.

When a major incompatibility is required:

  • launch the new version alongside the old one
  • provide migration documentation
  • instrument version usage
  • announce deprecation
  • provide a sunset window

Version lifecycle is covered deeply in Lofingo's separate API Versioning guide.

Design observability into the API surface

Track by route template, not raw IDs:

GET /orders/:id

not:

GET /orders/o_42
GET /orders/o_43

Useful metrics include:

  • request count
  • p50/p95/p99 latency
  • status class/code
  • request/response size
  • auth failures
  • rate-limit rejects
  • dependency latency
  • retry count

Do not put user IDs, tokens, arbitrary URLs, or raw search strings into metric labels.

Return request/trace IDs safely

A response header such as:

X-Request-ID: req_...

can help support/debugging when it maps to internal logs/traces.

Treat externally supplied IDs as untrusted input; validate or generate your own canonical correlation IDs.

Do not make sensitive internal database IDs part of public debugging metadata without a reason.

A production REST baseline

A strong baseline looks like:

  1. Domain-oriented stable resource URIs.
  2. Standard HTTP method semantics.
  3. Correct status codes.
  4. Idempotency protection for retryable non-idempotent operations.
  5. RFC Problem Details or another one consistent error contract.
  6. Strict schema/business validation and body-size limits.
  7. Resource-level authorization.
  8. Bounded cursor pagination for large mutable lists.
  9. Deliberate caching and ETag/precondition support where useful.
  10. Rate limits/quotas by meaningful caller and cost.
  11. OpenAPI contract + automated compatibility tests.
  12. Route-level metrics and distributed tracing.

Production checklist

Before shipping a REST endpoint, ask:

  • is the resource/action modeled clearly?
  • does the HTTP method match its semantics?
  • is retry behavior safe and documented?
  • are status codes meaningful to generic clients?
  • is the error format consistent?
  • are input type/size/range limits enforced?
  • is object/tenant authorization checked?
  • can list endpoints return unbounded data?
  • are cache headers safe for personalized responses?
  • can ETags/preconditions prevent lost updates or unnecessary transfers?
  • do rate limits reflect caller identity and operation cost?
  • does OpenAPI match real behavior?
  • can old clients survive additive changes?
  • do metrics avoid high-cardinality/sensitive labels?

REST APIs are strongest when they use HTTP as a real application protocol, not merely a tunnel for JSON. Correct method semantics, cache behavior, errors, preconditions, and authorization make APIs easier to integrate, retry, operate, and evolve.

References

Tags:RESTAPI DesignHTTPOpenAPIAPI SecurityBackend Development
ENTERPRISE SAAS ENGINEERING

BUILDING A SAAS PRODUCT?

Explore Lofingo’s end-to-end SaaS platforms, multi-tenant cloud architecture, and high-concurrency backend engineering.

EXPLORE SAAS SERVICES →