
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
“RESTful” is the most abused word in backend engineering.
Teams say “REST” when they mean:
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:
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.
REST is a set of constraints that (when you actually use them) create three things:
In practice, that means:
REST is an interface stability discipline.
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.
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/doPaymentThose are actions, and actions change when product changes.
Resources can survive those changes.
The fastest way to guarantee versioning is to ship database-shaped JSON.
If your API representation is a thin mirror of tables/columns:
A REST representation should be:
If your API is “SQL with HTTP,” you will version forever.
Most APIs use HTTP like a dumb pipe.
That’s leaving leverage on the table.
When teams misuse these semantics, clients and infrastructure can’t help them.
Example:
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.
A survivable URL scheme has three properties:
If an address is truly “inside” a parent:
/projects/{projectId}/environments/{envId}/orders/{orderId}/line-items/{itemId}If the parent is just a filter:
/customers/{id}/orders can be fine as a convenienceBecause 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=openYour “list” endpoints are where contracts die.
Because everyone wants “just one more filter” and suddenly you have:
A survivable approach:
GET /orders?status=open&createdAfter=...&sort=-createdAt&limit=50&cursor=...Offset pagination (page=10) is simple, but it lies under concurrent writes:
Cursor pagination is boring for clients but stable for systems:
Let’s be honest: systems have actions.
Pure REST can model actions as state transitions on resources, but the key is:
Two survivable patterns:
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.
Commands become resources you can inspect, retry, and audit:
POST /orders/{id}/cancellationsPOST /payments/{id}/capturesPOST /orders/{id}/refundsThis is incredibly survivable because:
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 are not decoration.
They’re how clients, tooling, and humans understand what happened.
A survivable mapping is boring:
200 OK — success with body201 Created — created new resource (include Location header if you can)202 Accepted — accepted for async processing204 No Content — success with no body400 Bad Request — client sent invalid data401 Unauthorized / 403 Forbidden — authn/authz404 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 limit5xx — server failure200 OK for everything and encode “success: false” in JSON, you just broke every proxy, cache, and monitoring system in your path.A survivable API has an error format with:
Example shape (conceptually):
code: "ORDER_INVALID_STATE"message: "Order cannot be cancelled after shipment"details: { currentStatus: "shipped" }traceId: "..."The format matters less than consistency.
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 cacheableHere’s the uncomfortable truth: most versioning is avoidable.
You version because:
So let’s talk about what actually works.
include=...expand=customer (only if you can support it reliably)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.
You’ll see three common approaches:
/v1/orders/...Accept: application/vnd.company.orders.v1+json?apiVersion=1 (least preferred)I’m not religious about the mechanism.
I’m religious about the reason you version.
If you want something you can paste into your team’s PR template, here it is.
A consistent API style lets teams onboard faster and ship without tribal knowledge.
When semantics are correct, your logs and traces make sense.
When semantics are nonsense, you end up debugging “ghost bugs.”
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.
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.
GraphQL is a different boundary: it moves composition to the client and offers a schema-driven contract.
It can be excellent — but it doesn’t remove the need for good boundaries, idempotency, authz, and operational discipline.
If your backend is chaotic, GraphQL just gives you a sharper knife.
Not always. URI versioning is simple and visible.
But don’t start by versioning “because everyone does.”
Start by designing for additive evolution, consistent representations, and safe deprecation.
If you still need versions, add them intentionally — and treat them as expensive to maintain.
The best one is the one your entire org uses consistently.
Pick a stable code, a human message, optional details, and always include a traceId.
Consistency is what buys you tooling, automation, and fast debugging.
When the action has meaningful lifecycle:
If you can’t answer “what is the identity of this action?” you’ll struggle to operate it under failure.
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:
Data Stores 101 for Architects: SQL, NoSQL, and the Shape of Consistency
Stop choosing databases by brand. Choose them by invariants, access patterns, and what “correct” means when the network is on fire.
Backends: Frameworks Don’t Matter Until They Do (Node, Java, .NET, Go, Python)
Early on, any backend “works.” Then timeouts, GC pauses, cold starts, and operability show up. This is a practical mental model for choosing runtimes and frameworks based on constraints—not vibes.