Designing Useful APIs: Best Practices for Naming, Idempotency, Pagination, Filtering, Versioning & Security


Best Practices in API Design (A Practical Guide)

APIs are the connective tissue of modern software. A useful API is predictable, consistent, and safe to evolve. Below is a concise, hands-on guide—mirroring the structure in the image—covering eight fundamentals with patterns, anti-patterns, and snippets you can drop into your docs.


1) Use Clear Naming

Principles

  • Nouns, not verbs for resources: /products/orders/123/users/42.
  • Plural collections; singular items: /products (list/create), /products/{id} (read/update/delete).
  • Use subresources for relationships/actions:
    • Relationship: /orders/123/items
    • Domain actions (state changes) as subpaths: /orders/123/cancel/users/42/verify
  • Consistent casing (kebab-case or snake_case) and predictable errors.

Do

POST   /api/v1/products
GET    /api/v1/products?category=shoes
PATCH  /api/v1/products/123
DELETE /api/v1/products/123

Don’t

/createNewProduct
/getProductsList
/Products/GetById?id=123

2) Idempotency

Some HTTP methods are inherently idempotent (GET, HEAD, PUT, DELETE). POST usually isn’t—so make it behave idempotently where duplicates would hurt (e.g., payments, orders) using an idempotency key.

Pattern

  • Client generates a unique key and sends it with the request.
  • Server stores the result (or hash) keyed by that token for a time window.
  • Replays with the same key return the original response.

Example

curl -X POST https://api.example.com/v1/orders   -H "Idempotency-Key: 7f9a2b3c-1e4d-4b36-b8b1-6d7a3caa9b7e"   -H "Content-Type: application/json"   -d '{"cart_id":"c_123","payment_method":"pm_987"}'

Tips

  • Return 409 Conflict or 412 Precondition Failed for conflicting replays.
  • Make keys opaque to the server and unique per operation.

3) Pagination

Prevent payload bloat and hotspot queries by paging your lists. Two common strategies:

Offset-based

  • Simple mental model: ?limit=50&offset=100
  • Works well for small data sets.
  • Can be inaccurate if rows are inserted/deleted rapidly.
GET /v1/products?limit=25&offset=50&sort=created_at

Cursor-based

  • Uses a stable pointer (e.g., last item’s sort key).
  • Reliable under heavy writes; recommended for large or real-time feeds.
GET /v1/products?limit=25&cursor=eyJpZCI6IjEyMyIsImNyZWF0ZWRfYXQiOiIyMDI1LTA4LTE3In0=

Always include pagination metadata & links

200 OK
Link: <.../products?cursor=abc>; rel="next",
      <.../products?cursor=>; rel="prev"
{
  "data": [...],
  "page": { "limit": 25, "next_cursor": "abc" }
}

4) Sorting and Filtering

Expose explicit, whitelisted fields; keep syntax predictable.

GET /v1/products
  ?filter[category]=running
  &filter[price_lte]=100
  &sort=-created_at,price
  &fields= id,name,price,thumbnail_url

Notes

  • Use -field to indicate descending.
  • Support compound sorts and typed filters (e.g., _gte_lte_in).
  • Validate unknown filters and reject with 400 to prevent silent surprises.

5) Cross-Resource References

Prefer resource paths over long query strings when referencing related entities:

Prefer

/api/v1/carts/123/items/321

Avoid (harder to read/cache)

/api/v1/items?cart_id=123&item_id=321

When creating dependent resources, return 201 Created with a Location header to the canonical URL:

Location: /api/v1/carts/123/items/321

Also consider including link objects in payloads:

{
  "id": "321",
  "sku": "SKU-1",
  "links": { "cart": "/api/v1/carts/123" }
}

6) Rate Limiting

Protect reliability by capping request throughput per token/IP/account. Communicate limits clearly.

Status & headers

  • Use 429 Too Many Requests when exceeded.
  • Include modern rate limit headers (RFC 9332 style):
RateLimit-Limit: 100;w=3600
RateLimit-Remaining: 42
RateLimit-Reset: 1723872000
Retry-After: 120

Good practices

  • Different buckets by endpointuserorg, or write vs read.
  • Offer exponential backoff guidance in docs.
  • Log & alert on sustained 429s.

7) Versioning

You need versioning only for breaking changes. Prefer backwards-compatible evolution (add fields, not remove/rename).

Common strategies

  • URI versioning (simple, explicit)
    /api/v1/users
    /api/v2/users
    
  • Media type (Accept header)
    Accept: application/vnd.example.v2+json
    

Deprecation workflow

  • Announce in changelog.
  • Send Deprecation: true and Sunset headers on old endpoints.
  • Provide a migration guide and dual-serve for a grace period.

8) Security

Security is non-negotiable. Apply defense-in-depth:

Transport & boundaries

  • TLS everywhere (HTTPS only). Redirect and HSTS.
  • Validate content types; enforce sane payload sizes.

AuthN / AuthZ

  • Support API keys for server-to-server use; OAuth2/OIDC for user-delegated flows; JWT for stateless sessions.
  • Use scopes/roles with least privilege (e.g., products:readorders:write).
  • Rotate keys, set short JWT expirations, and prefer PKCE for public clients.

Example

Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...

Input & data handling

  • Validate and sanitize inputs (JSON Schema).
  • Consistent error shapes; avoid leaking internals.
  • Store secrets in a vault; never in code or logs.
  • Audit logging for sensitive actions; enable request tracing.
  • Protect against replay (nonces, idempotency keys), injection (parameterized queries), and object-level auth (check ownership).

Quick Checklist

  • Location set on 201s.

Reference Snippets (copy/paste)

Standard response envelope

{
  "data": {...},
  "errors": [],
  "meta": { "request_id": "req_abc123", "trace_id": "00-..." },
  "links": { "self": "/v1/products/123" }
}

Error shape

{
  "errors": [
    {
      "code": "validation_failed",
      "title": "Invalid filter",
      "detail": "Unknown filter 'colour'",
      "source": { "parameter": "filter[colour]" }
    }
  ]
}

CORS (sane defaults)

Access-Control-Allow-Origin: https://your.app
Access-Control-Allow-Methods: GET,POST,PATCH,DELETE,OPTIONS
Access-Control-Allow-Headers: Authorization,Content-Type,Idempotency-Key

Final Thoughts

Designing useful APIs boils down to clarity, safety, and evolvability. If you implement the eight pillars above—and document them with real examples—your API will be predictable for consumers today and resilient to change tomorrow.

Comments

Popular posts from this blog

Mount StorageBox to the server for backup

psql: error: connection to server at "localhost" (127.0.0.1), port 5433 failed: ERROR: failed to authenticate with backend using SCRAM DETAIL: valid password not found

Keeping Your SSH Connections Alive: Configuring ServerAliveInterval and ServerAliveCountMax