Blog
Aug 29, 2021 - 16 MIN READ
RESTful Design That Survives: Resources, Boundaries, and Versioning

RESTful Design That Survives: Resources, Boundaries, and Versioning

REST isn’t “JSON over HTTP.” It’s a set of constraints that make interfaces boring, predictable, and resilient under change. This month is about designing resource boundaries and contracts that survive growth — and using versioning only when you’ve earned it.

Axel Domingues

Axel Domingues

“RESTful” is the most abused word in backend engineering.

Teams say “REST” when they mean:

  • “We have endpoints.”
  • “We return JSON.”
  • “We use HTTP.”

And that’s how you end up with an API that works for the demo… and collapses the moment you try to evolve it.

Because the real point of REST is survivability:

  • the client can change without breaking the server
  • the server can change without breaking the client
  • the interface stays boring, predictable, and cacheable
  • failures are diagnosable, not mystical

This post is not about purity.

It’s about making interfaces that survive more features, more teams, and more years — without turning into “v2/v3/v4” forever.

The goal this month

Design REST APIs that are stable under change: resource boundaries, HTTP semantics, and contracts that evolve without drama.

The mental model

Think in resources + representations + links + state transitions, not “controllers + endpoints.”

The hard truth

Versioning is not a strategy.
It’s what you do when you failed to design for evolution.

The output

A practical checklist you can apply to any API you touch next week.


What REST Actually Buys You

REST is a set of constraints that (when you actually use them) create three things:

  1. Predictability — the same patterns everywhere
  2. Loose coupling — clients don’t know server internals
  3. Evolution — change happens by adding, not breaking

In practice, that means:

  • resources are nouns (“orders”, “invoices”, “users”)
  • clients operate on representations of those resources (JSON, etc.)
  • HTTP methods are not decorative — they encode intent + semantics
  • responses are cacheable when they should be
  • errors are consistent and machine-readable
  • the “shape of the API” doesn’t mutate every sprint
If you remember one sentence:

REST is an interface stability discipline.


Step 0: Stop Designing Endpoints. Start Designing Boundaries.

Most “REST problems” are actually boundary problems.

You don’t have a versioning problem.
You have a “we coupled the client to internal tables” problem.

You don’t have a pagination problem.
You have a “we treated a query as a resource” problem.

You don’t have a “backend is slow” problem.
You have a “we put cross-service composition into a chatty API” problem.

So before we talk about URLs, we need to talk about ownership.

Boundary question

What does this service own and what is it merely referencing?

Contract question

What does the client need to do, and what does it not need to know?

A strong API design starts with a boring statement:

“This service owns Orders. It exposes Order resources.
It references Customers by ID, but Customer data is not part of this service’s contract.”

That one sentence prevents half of your future breaking changes.


Resources: Choose Nouns That Don’t Lie

A resource is a stable “thing” in your domain with an identity.

Good examples:

  • /orders/{orderId}
  • /customers/{customerId}
  • /invoices/{invoiceId}
  • /projects/{projectId}

Bad (fragile) “resources” are often verbs or UI workflows:

  • /createOrder
  • /submitCheckout
  • /getOrderList
  • /doPayment

Those are actions, and actions change when product changes.
Resources can survive those changes.

If your API reads like a UI flowchart, you’re building a workflow API.
That can be fine — but don’t call it REST, and don’t expect it to evolve cleanly.

Representations: Your JSON Is Not Your Database

The fastest way to guarantee versioning is to ship database-shaped JSON.

If your API representation is a thin mirror of tables/columns:

  • you leak internal model decisions
  • every schema refactor becomes a breaking change
  • clients start depending on “accidental fields”

A REST representation should be:

  • purposeful (fields exist because clients need them)
  • stable (field names don’t bounce around)
  • extensible (new fields can be added without breaking clients)
  • consistent across endpoints
Your database is an implementation detail.

If your API is “SQL with HTTP,” you will version forever.


HTTP Semantics: Use the Protocol You’re Standing On

Most APIs use HTTP like a dumb pipe.

That’s leaving leverage on the table.

Safe vs Idempotent vs “Don’t Repeat That Request”

  • GET is safe and idempotent
    • safe: should not change server state
    • idempotent: repeated calls are equivalent (same effect)
  • PUT is idempotent (replace entire resource or set to a known state)
  • PATCH is usually idempotent if designed carefully (more on that soon)
  • POST is not idempotent by default (create / submit / trigger)

When teams misuse these semantics, clients and infrastructure can’t help them.

Example:

  • if you use POST for “update,” you just removed retry safety
  • if you mutate state on GET, you broke caching and observability

Rule of thumb

If a request might be retried (and it will), design it to be idempotent or provide an idempotency key.

Ops consequence

Bad HTTP semantics turn transient failures into data corruption.


URLs: Think Like a Map, Not Like a Router

A survivable URL scheme has three properties:

  1. it’s hierarchical when it reflects containment/ownership
  2. it’s flat when containment is accidental
  3. it’s boring enough that nobody bikesheds it every sprint

When hierarchy is real

If an address is truly “inside” a parent:

  • /projects/{projectId}/environments/{envId}
  • /orders/{orderId}/line-items/{itemId}

When hierarchy is fake (don’t do this)

If the parent is just a filter:

  • /customers/{id}/orders can be fine as a convenience
  • but don’t make it the only way to address orders

Because an Order exists even when you stop caring about Customers.

A good rule:

If the child has its own lifecycle and identity, it needs its own top-level address too.

So you can have both:

  • /orders/{orderId}
  • /customers/{customerId}/orders?status=open

Querying: Filters Are Not Resources (But They Are a Contract)

Your “list” endpoints are where contracts die.

Because everyone wants “just one more filter” and suddenly you have:

  • 40 query parameters
  • undefined combinations
  • inconsistent sorting/pagination
  • surprising performance cliffs

A survivable approach:

  • keep list endpoints predictable
  • define a small set of supported filters
  • document which combinations are valid
  • design pagination and sorting once and reuse it
If you’re building UX that needs “jump to page 37”, you’re often building the wrong UX.
For operational tools, you can still support offset — but treat it as a deliberate tradeoff.

Writes: Model Commands Without Becoming RPC

Let’s be honest: systems have actions.

  • “submit order”
  • “cancel order”
  • “capture payment”
  • “issue refund”

Pure REST can model actions as state transitions on resources, but the key is:

  • keep the resource identity central
  • keep the state machine explicit
  • avoid “endpoint-per-button” sprawl

Two survivable patterns:

Pattern A: State transition as PATCH

PATCH /orders/{id} with a representation of the change:

  • { "status": "cancelled", "reason": "customer_request" }

This is clean when “status” is part of the domain model and the transition rules live server-side.

Pattern B: Commands as sub-resources

Commands become resources you can inspect, retry, and audit:

  • POST /orders/{id}/cancellations
  • POST /payments/{id}/captures
  • POST /orders/{id}/refunds

This is incredibly survivable because:

  • the action has an identity
  • you can store its state (pending/succeeded/failed)
  • clients can poll or subscribe to it
  • retries are safe and observable

Choose PATCH when…

The change is a normal update to the resource’s state, and you want a simple contract.

Choose command resources when…

The action needs auditability, retries, or async processing — and “just update a field” would hide complexity.


Status Codes: Don’t Be Cute (Be Specific)

Status codes are not decoration.
They’re how clients, tooling, and humans understand what happened.

A survivable mapping is boring:

  • 200 OK — success with body
  • 201 Created — created new resource (include Location header if you can)
  • 202 Accepted — accepted for async processing
  • 204 No Content — success with no body
  • 400 Bad Request — client sent invalid data
  • 401 Unauthorized / 403 Forbidden — authn/authz
  • 404 Not Found — resource doesn’t exist (or you don’t want to reveal it)
  • 409 Conflict — state conflict (optimistic concurrency, duplicate create, etc.)
  • 422 Unprocessable Entity — domain validation (optional, but be consistent)
  • 429 Too Many Requests — rate limit
  • 5xx — server failure
If you return 200 OK for everything and encode “success: false” in JSON, you just broke every proxy, cache, and monitoring system in your path.

Error Format: Make Failures Machine-Readable

A survivable API has an error format with:

  • a stable error code
  • a human-friendly message
  • optional details (field errors, metadata)
  • a trace/correlation id for debugging
  • a link to documentation (optional but helpful)

Example shape (conceptually):

  • code: "ORDER_INVALID_STATE"
  • message: "Order cannot be cancelled after shipment"
  • details: { currentStatus: "shipped" }
  • traceId: "..."

The format matters less than consistency.

If you want speed-of-debugging, treat “error shape” as part of your contract — and version it even more carefully than success responses.

Caching: The Hidden Performance Budget

REST’s secret weapon is that it can be cache-friendly by design.

Even if you don’t build a CDN strategy today, your future self will thank you if you design for it:

  • GET endpoints are safe and cacheable
  • responses include cache headers (or explicitly say “don’t cache”)
  • you use ETags (or timestamps) to validate freshness
  • you avoid “GET changes state” booby traps
Caching is not an optimization.
It’s an architectural capability that changes cost, latency, and scalability.

Versioning: The Last Resort You Earn

Here’s the uncomfortable truth: most versioning is avoidable.

You version because:

  • clients depend on fields you didn’t mean to publish
  • you changed semantics instead of adding capabilities
  • you mixed concerns and now can’t evolve independently

So let’s talk about what actually works.

A survivable evolution strategy (in order)

Add, don’t break

  • Add new fields (clients ignore what they don’t know)
  • Add new endpoints/resources
  • Add new query parameters (with safe defaults)

Use explicit capability flags (when needed)

  • Opt-in parameters like include=...
  • Expand patterns like expand=customer (only if you can support it reliably)

Deprecate with telemetry

  • Emit deprecation headers / warnings
  • Track client usage of deprecated fields/paths
  • Provide a migration guide
  • Remove only when you have evidence

Version only when semantics must change

If you must change meaning (not just shape), you may need a new contract.
But this should be rare — and expensive enough that you do it with intention.

Where to put the version (if you must)

You’ll see three common approaches:

  • URI versioning: /v1/orders/...
  • Header/content negotiation: Accept: application/vnd.company.orders.v1+json
  • Query param: ?apiVersion=1 (least preferred)

I’m not religious about the mechanism.
I’m religious about the reason you version.

If your plan is “we’ll just do /v2”, you’re planning to ship breaking changes forever.
A mature org treats versioning like a controlled burn — not a lifestyle.

A Practical REST Survivability Checklist

If you want something you can paste into your team’s PR template, here it is.


Field Notes: Why “Boring REST” Wins in Real Teams

1) It scales across engineers, not just across machines

A consistent API style lets teams onboard faster and ship without tribal knowledge.

2) It makes debugging faster than heroic observability

When semantics are correct, your logs and traces make sense.
When semantics are nonsense, you end up debugging “ghost bugs.”

3) It reduces frontend-backend friction

Frontends don’t want “perfect REST.”
They want predictable contracts and safe evolution.

If you give them that, they won’t care whether your endpoint is “pure.”

August takeaway

REST is not “endpoints.”
REST is a discipline that makes interfaces boring, evolvable, and debuggable.


Resources

RFC 9110 — HTTP Semantics

The canonical definition of HTTP method semantics, status codes, and caching behaviors. If you’re designing contracts, this is the ground truth.

Microsoft REST API Guidelines

A pragmatic set of conventions for resource modeling, error shapes, long-running operations, and compatibility. Useful as an organizational baseline.

Zalando RESTful API Guidelines

Another excellent “boring by design” guideline set — especially strong on compatibility, naming, and consistency rules.

Roy Fielding’s REST dissertation

The original architectural definition. You don’t need to be a purist, but reading it once makes the tradeoffs clearer forever.


FAQ


What’s Next

Now that we’ve made the API boundary survivable, the next month is about what sits behind it:

Data Stores 101 for Architects

Because the moment your API becomes stable, you hit the next reality:

Your contract can be boring… while your data layer is anything but.

We’ll cover:

  • when relational modeling wins (and why it keeps winning)
  • what NoSQL actually buys you (and what it costs)
  • consistency models in plain language
  • and how data choices shape your API’s semantics and your system’s failure modes
Axel Domingues - 2026