Welcome to Part IX. Everything from here on assumes a system made of many independently deployed pieces — and REST is the contract that holds them together.
Lesson 132 taught you REST from the outside: you were a client calling one API, reading its JSON, matching its verbs to its status codes. Lessons 253–258 taught you REST from the inside: you built one ASP.NET Core API — routes, controllers, model binding, validation — all of it living inside a single deployable project.
Now flip the picture. Imagine OrderService, InventoryService, and PaymentService as three separate applications, built by three different teams, deployed on three different schedules, each with its own database. The order API isn't just "an interface a client happens to call" anymore — it's a contract that other independently-deployed services depend on to keep working. Change it carelessly, and you don't just break one client's code review — you break a service you may not even know exists, running in someone else's deployment pipeline, on someone else's schedule.
This lesson is the front door to Part IX — Distributed Systems. Everything after it — microservices, message queues, Kafka, event-driven architecture, and the resilience patterns that follow — is about systems built from many independently deployed pieces that must communicate reliably. REST API architecture is where that story starts, because HTTP APIs are still the most common way two services first learn to talk to each other.
REST API architecture is REST's design conventions applied at the scale of a whole system, not just one endpoint. Lesson 132 asked "what does GET /orders/4471 mean?" This lesson asks bigger questions: How "RESTful" does an API actually need to be to be useful? How does an API change over time without breaking every service that already depends on it? How do you design resources so that a completely separate team, building a completely separate service, can predict your API's shape without a phone call?
Resource-oriented design means every URL names a noun — a thing the system manages — and every operation on it is expressed through HTTP's existing verbs and status codes, exactly as lesson 132 covered. At the architectural level, two additional concerns become unavoidable the moment more than one team or more than one deployable is involved:
By the time REST became the dominant convention for HTTP APIs, the word "REST" had drifted from Roy Fielding's original, fairly strict architectural style into a much looser industry habit: "any JSON API over HTTP." Two APIs can both honestly call themselves "RESTful" while being wildly different in how much of the actual REST philosophy they implement — one might expose a single POST /api endpoint that takes an "action" field in the body, while another exposes dozens of well-designed resource URLs with correct verb and status-code usage. Without a shared vocabulary for how RESTful an API is, teams have no precise way to describe or evaluate that difference — "it's REST" tells you almost nothing.
Inside a single deployable, changing a method's signature and fixing every caller is one atomic commit. Across services, that's no longer true. If InventoryService renames a field in its GET /stock/{sku} response, and OrderService — deployed by a different team, on a different day — is still reading the old field name, OrderService breaks in production the moment InventoryService's change ships, with no compiler, no shared build, and often no advance warning to catch it.
The Richardson Maturity Model gives teams precise, shared language for how RESTful an API genuinely is, so "we're building a REST API" means something specific. Versioning strategies give teams a deliberate mechanism for evolving an API's contract over time without silently breaking every consumer the moment a change ships.
Every lesson from here through the end of Part IX assumes exactly this situation: a system made of independently deployed pieces, communicating over a network, that cannot be redeployed together as one atomic unit. REST API architecture is the first and most familiar instance of that theme — it just happens to use HTTP request/response instead of the asynchronous messaging covered starting in lesson 286.
Level 0 — THE SWAMP OF POX
One single endpoint. Everything tunneled through POST.
POST /api { "action": "getOrder", "orderId": 4471 }
Level 1 — RESOURCES
Separate URLs per resource — but still mostly one verb (POST) for everything.
POST /orders/4471/get
POST /orders/4471/cancel
Level 2 — HTTP VERBS + STATUS CODES ← most real-world "REST" APIs live here
GET /orders/4471 → 200 OK
POST /orders → 201 Created
DELETE /orders/4471 → 204 No Content, or 404 Not Found
Level 3 — HYPERMEDIA CONTROLS (HATEOAS)
Response body embeds links telling the client what it can do next:
{ "orderId": 4471, "status": "Placed",
"_links": { "cancel": "/orders/4471/cancel", "pay": "/orders/4471/pay" } }
The levels are cumulative — Level 2 assumes you already have Level 1's resource-oriented URLs, and adds correct verb/status-code usage on top. Almost every API you've used in this course, and almost every production API you'll integrate with professionally, sits at Level 2. That is not a failure to reach Level 3 — it's simply where the practical value-to-effort ratio tends to land, a point worth being honest about rather than treating Level 3 as the "correct" target everyone is falling short of.
/orders/4471, /customers/900 — instead of one shared endpoint.GET /v1/orders/4471
GET /v2/orders/4471
GET /orders/4471
X-Api-Version: 2
GET /orders/4471
Accept: application/vnd.myapp.order.v2+json
Compare the same operation — cancel an order — at Level 0 versus Level 2:
// ─── Level 0 — everything through one POST endpoint ───
POST /api
{
"action": "cancelOrder",
"orderId": 4471
}
// Response is always 200 OK — success/failure is a field buried in the body:
{ "success": false, "error": "Order already shipped" }
// ─── Level 2 — a real resource, a real verb, a real status code ───
DELETE /orders/4471
// 204 No Content → cancelled successfully
// 409 Conflict → order already shipped, cannot cancel
// 404 Not Found → order 4471 doesn't exist
Meaning: At Level 0, an HTTP-aware tool — a proxy, a monitoring dashboard, a cache — has no way to tell success from failure without parsing the body; every response looks identical (200 OK) at the transport level. At Level 2, the outcome is visible in the status code alone, which is exactly why infrastructure that only understands HTTP semantics (load balancers, API gateways, monitoring) can reason about a Level 2 API without knowing anything about its business domain.
Picture OrderService exposing GET /v1/orders/{id}, consumed independently by a mobile app, a partner integration, and an internal ReportingService. The team decides totalAmount should split into subtotal and tax for a new tax-reporting requirement — a genuine breaking change to the response shape.
// v1 — the existing contract; every current consumer relies on this exact shape
GET /v1/orders/4471
{ "orderId": 4471, "totalAmount": 108.00 }
// v2 — the new shape, published alongside v1, not instead of it
GET /v2/orders/4471
{ "orderId": 4471, "subtotal": 100.00, "tax": 8.00 }
Because OrderService versioned the URL instead of silently editing v1's response, the mobile app and the partner integration keep working, unmodified, exactly as they were the day before — while ReportingService's team migrates to v2 on their own schedule, whenever they're ready. v1 is typically kept running for a deliberately-communicated deprecation window before it's finally retired — not deleted the moment v2 ships.
Calling an API you own, from code you also own, is like talking to a friend — if you misremember what you said last week, you can just clarify in person. Calling an API that other independently-deployed teams depend on is like a signed legal contract: once the other party has built their own systems around specific clauses (specific field names, specific status codes), you can't casually renegotiate those clauses without warning. Versioning is how you introduce an amended contract (v2) while honoring the original one (v1) for everyone who already signed it, until they've had a fair chance to migrate.
URL versioning in ASP.NET Core is typically just ordinary attribute routing with the version baked into the route template — [Route("v1/orders")] on one controller and [Route("v2/orders")] on another, or a dedicated versioning library that maps requests to the correct controller/action pair based on the URL segment, header, or media type it finds. Under the hood, there's no special HTTP-level machinery involved — the version identifier is just another piece of routing information the framework's routing middleware uses to select which endpoint handles the request, the same routing mechanism from lesson 254 that's always been dispatching every request in this course.
What actually changes between v1 and v2 is usually not the underlying domain logic at all — both versions likely call into the same OrderService business logic — but the shape of the DTO each version serializes to JSON. This is precisely why Clean Architecture's dependency rule (lesson 249) pays off here: if the domain and application layers don't know about HTTP DTOs at all, adding a v2 response shape is purely a presentation-layer concern, isolated from the actual business rules underneath it.
Level 3 is the theoretically complete form of REST as Fielding originally described it, but the overwhelming majority of APIs described as "RESTful" in the industry — including most APIs you'll build and consume professionally — stop at Level 2 and are still entirely reasonably called REST APIs in ordinary conversation. Insisting Level 2 APIs "aren't really REST" is technically defensible pedantry that doesn't match how the term is used in practice. Know the distinction, but don't be surprised when almost nothing you encounter reaches Level 3.
Adding v2/orders doesn't imply a second copy of the orders table, or two separate OrderService deployments. Most commonly, one running service exposes both v1 and v2 endpoints against the exact same underlying data and business logic — only the request/response DTO shape differs between them.
Renaming or removing a field on an already-consumed endpoint and redeploying, assuming "no one will notice" — every consumer parsing that field breaks in production, silently, at a time you don't control.
Treat any change that removes, renames, or changes the type/meaning of an existing field as a breaking change requiring a new version — additive changes (a genuinely new, optional field) are usually safe without one.
Tunneling every operation through POST /api with an "action" field, then labeling it REST in the documentation — this loses every benefit HTTP-aware infrastructure (caches, gateways, monitoring) could otherwise give you for free.
Reach Level 2 deliberately — resource URLs, correct verbs, correct status codes — since that's genuinely where most of REST's practical value lives.
Deleting v1 the day v2 deploys, with no advance notice — this breaks every consumer who hasn't migrated yet, defeating the entire purpose of versioning in the first place.
Communicate a deprecation window, keep the old version running through it, and only retire it once consumers have genuinely had time to migrate.
You've seen how RESTful an API can actually be, and why it needs to survive change once other services depend on it. Let's confirm the details.
1. An API exposes a single POST /api endpoint. Every operation is described by an "action" field in the JSON body, and the response is always 200 OK with a "success" flag inside it. Where does this API sit on the Richardson Maturity Model?
Correct: C
Why C is correct: A single endpoint with the operation encoded inside the request body, and outcomes reported inside a 200 OK body rather than through the status code, is the textbook description of Level 0 — HTTP is used purely as a transport, none of its semantics are actually used.
Why A is incorrect: Using JSON as a format has nothing to do with hypermedia controls — Level 3 requires embedded links describing available next actions, which this API doesn't have.
Why B is incorrect: Always returning 200 OK regardless of outcome is the opposite of correct status-code usage — a defining requirement of Level 2.
Why D is incorrect: This is exactly the kind of API the Richardson Maturity Model was designed to classify.
Reinforcement: Tunneling everything through one endpoint and one verb, with outcomes hidden in the body, is Level 0 — the "swamp of POX."
2. Which statement about Level 3 (HATEOAS) is the most accurate, according to this lesson?
Correct: B
Why B is correct: This lesson is explicit that Level 3 is the theoretical "complete" form of REST, but that the overwhelming majority of production APIs — including nearly everything you'll build and consume professionally — stop at Level 2.
Why A is incorrect: In everyday industry usage, Level 2 APIs are routinely and reasonably described as "RESTful" — insisting otherwise is technically defensible but doesn't match common usage.
Why C is incorrect: HATEOAS is a format-agnostic idea about embedding navigational links in a response, not tied to a specific data format.
Why D is incorrect: Versioning and HATEOAS solve different problems — versioning is about contract evolution, HATEOAS is about client-driven navigation. Neither replaces the other.
Reinforcement: Know the theoretical model, but don't expect it in practice — that gap is normal, not a failure.
3. Why does API versioning matter specifically once other independently-deployed services depend on an API, in a way it didn't matter as much for a single, self-contained application?
Correct: C
Why C is correct: This is the lesson's central argument — a contract shared across independently-deployed services can't be edited and redeployed as one atomic change the way an internal method signature can. Versioning lets the old contract keep working for consumers who haven't migrated yet.
Why A is incorrect: HTTP itself has no requirement around version numbers in URLs — URL versioning is a convention teams adopt, not an HTTP rule.
Why B is incorrect: JSON serialization works fine with or without a version field; that's unrelated to why versioning matters here.
Why D is incorrect: Versioning is an application-level design decision, not a compiler requirement.
Reinforcement: Versioning exists to protect consumers you can't coordinate with directly — that's the defining condition of a distributed system.
4. A team wants to keep their resource URLs completely stable across versions, on the reasoning that a resource's identity (its URL) shouldn't change just because its representation does. Which versioning strategy best matches that goal?
Correct: B
Why B is correct: Header versioning and content-negotiation versioning both keep the resource's URL identical across versions, encoding the version in a header or the Accept value instead — exactly matching a team that wants URL identity to stay stable.
Why A is incorrect: URL versioning deliberately changes the path between versions — that's the opposite of what this team wants, even though it's the more common and more discoverable strategy overall.
Why C is incorrect: This still changes the URL, just with a different naming pattern — it doesn't achieve URL stability either.
Why D is incorrect: Header and content-negotiation versioning exist specifically to solve this; the lesson covers both.
Reinforcement: Each versioning strategy trades off differently between discoverability and strict resource-identity purity — pick based on what actually matters for your consumers.
You now understand REST as an architectural contract between independently-deployed services, not just a single API's shape. Next up — Part IX continues with the architecture that decides how many independently-deployed services you'll even have in the first place.
dotnetmadeeasy.com — Learn C# and .NET, the right way.