N
Naveenr.dev
Chapter 05
5 min read2026-06-17

RESTful API Design & Versioning Strategies

Why a "quick" field rename in your API broke three mobile clients, and how resource modeling, pagination, versioning, and rate limiting prevent that from happening again.

Someone on your team renames a field in the /users response because the old name was confusing. Ships it. Two hours later, the mobile app — which you don't control the release cadence of, because it's sitting in an app store review queue — starts throwing null-pointer exceptions for every user on an older app version. The API didn't break; it changed, and something else was depending on it not changing.

This is the actual reason API design discipline exists. It's not about following REST conventions for their own sake — it's that your API is a contract with clients you don't always control, and every decision below exists to either make that contract clearer or let it evolve without breaking anyone.

REST Principles Recap

REST (Representational State Transfer) is an architectural style built on HTTP. The parts that actually matter day-to-day:

  1. Client-Server: Clear separation of concerns — the client doesn't need to know how your data is stored, and you don't need to know how the client renders it.
  2. Statelessness: Each request carries everything it needs; no session state lives on the server between requests.
  3. Cacheable: Responses should say whether they're cacheable — this is what lets a CDN or client cache take load off your servers.
  4. Uniform Interface: Consistent design across resources, so a developer who understands /users can guess how /orders works.

Resource Modeling

Design your API around resources — nouns — not actions. This isn't a style preference; action-oriented endpoints tend to multiply endlessly (/createUser, /updateUserEmail, /updateUserPassword...) while resource-oriented ones compose.

✅ Good (Resource-Oriented)

POST   /users                 # Create a user
GET    /users                 # List all users
GET    /users/{id}            # Get a specific user
PUT    /users/{id}            # Update a user
DELETE /users/{id}            # Delete a user
GET    /users/{id}/posts      # Get posts by a user
POST   /users/{id}/posts      # Create a post for a user

❌ Bad (Action-Oriented)

POST   /createUser
GET    /getUser/{id}
POST   /updateUser
POST   /deleteUser/{id}
GET    /getUserPosts/{id}

HTTP Status Codes

Status codes exist so clients can react programmatically without parsing an error message string. Using them inconsistently is one of the fastest ways to make an API frustrating to integrate against.

CodeMeaningWhen to Use
200OKSuccessful GET, PUT, DELETE
201CreatedSuccessful resource creation (POST)
204No ContentSuccessful request with no response body
400Bad RequestInvalid input, malformed JSON
401UnauthorizedMissing/invalid authentication token
403ForbiddenAuthenticated, but lacks permission
404Not FoundResource doesn't exist
429Too Many RequestsRate limit exceeded
500Internal Server ErrorUnrecoverable server error
503Service UnavailableServer temporarily down (maintenance, overload)

Pagination

For endpoints returning large result sets, pagination isn't optional — without it, a client that wants "recent orders" ends up asking your database to hand over a table's worth of rows.

Offset-Based Pagination

GET /users?limit=20&offset=40
  • Simple to implement, easy to reason about.
  • Problem: Instability. If a new row is inserted while a client is paging through, offsets shift and results duplicate or get skipped between pages.
GET /users?limit=20&cursor=eyJpZCI6IDUwfQ==
  • The cursor encodes the exact position in the result set (e.g., an encoded {"id": 50}).
  • Advantage: Stable even with concurrent inserts and deletes — this is why every high-traffic feed (social media timelines, real-time data streams) uses cursors instead of offsets.

API Versioning

This is the direct fix for the field-rename incident at the top of this chapter. Once you have external clients you don't fully control, you can't just change a response shape — you version it, and let old clients keep working against the old version while new clients get the new one.

1. URL Path Versioning

GET /v1/users
GET /v2/users  # Different response schema
  • Pros: Explicit and impossible to miss — anyone reading a log or a bug report immediately knows which version was hit.
  • Cons: You're maintaining parallel code paths, which is real ongoing cost.

2. Header Versioning

GET /users
Header: API-Version: 2
  • Pros: URLs stay clean; versioning is metadata, not part of the resource path.
  • Cons: Less discoverable — a client has to already know to send the header, and it's easy to forget when testing manually.

3. Query Parameter Versioning

GET /users?api_version=2
  • Pros: Easy to test manually, visible directly in a browser URL or a shared link.
  • Cons: Verbose, and it clutters the query string alongside actual filtering parameters.

Deprecation Strategy

  • Announce deprecation 6–12 months before removal — mobile clients especially can't update on your schedule.
  • Return Deprecation and Sunset headers so automated tooling can flag it, not just humans reading changelog emails.
  • Reach out to your highest-volume clients directly instead of assuming they'll notice a changelog entry.

Rate Limiting

Rate limiting exists to protect your API from being overwhelmed by a single client — whether that's abuse, a misbehaving retry loop, or just one customer's integration being far more active than everyone else's.

Token Bucket Algorithm

  • Each client gets a "bucket" of tokens.
  • Each request consumes one token.
  • Tokens regenerate at a fixed rate (e.g., 100 tokens/minute).
  • If the bucket is empty, reject the request with 429 Too Many Requests.

Response Headers

Tell the client where they stand instead of letting them find out by getting rejected:

HTTP/1.1 200 OK
RateLimit-Limit: 1000
RateLimit-Remaining: 999
RateLimit-Reset: 1618329600

Error Handling

A consistent, structured error response is what lets a client handle failures programmatically instead of showing users a raw stack trace or a generic "something broke" message.

Good Error Response

{
  "error": {
    "code": "INVALID_EMAIL",
    "message": "The provided email is not a valid format",
    "details": {
      "field": "email",
      "received": "not-an-email"
    }
  }
}

Bad Error Response

{
  "error": "Something went wrong"
}

Key Takeaways

  • API design discipline exists because your API is a contract with clients you don't fully control — a "quick" breaking change can take down clients that can't update on your timeline.
  • Resource-oriented endpoints (nouns, not verbs) compose predictably; action-oriented endpoints multiply endlessly.
  • Cursor-based pagination is the only approach that stays correct under concurrent writes — offset pagination silently duplicates or skips rows under real traffic.
  • Versioning and deprecation windows exist specifically for clients you can't force to upgrade on your schedule, like mobile apps sitting in review queues.
  • Rate limiting protects the system from any single client — malicious or just misbehaving — from degrading service for everyone else.

Next Steps

These five chapters cover the building blocks most systems are made of: caching, event streaming, databases, load balancing, and API design. The chapters after this move up a level — into the foundational trade-offs (the CAP theorem, consistency models, scalability patterns) and real "design X" walkthroughs (a URL shortener, a rate limiter, a notification system) that combine everything covered so far into complete systems.

Enjoyed this chapter?

Get an email when I publish the next chapter. No spam — just new technical deep-dives.

Comments

Share feedback or questions about this blog post.

No comments yet. Be the first to share your thoughts.