Representational State Transfer in a Real API Integration
Fielding's six constraints rebuilt step-by-step through a working payment API.
Roy Fielding wrote his dissertation in 2000, describing an architectural style for distributed hypermedia systems. REST as most developers know it today borrowed heavily from that work, but drifted from it, too. The gap between the two is bigger than most API documentation admits, and closing it is the whole point of what follows.
Fielding named six constraints: Client-Server, Stateless Communication, Cache, Uniform Interface, Layered System, and Code-On-Demand. Uniform Interface splits into four sub-constraints: Identification of Resources, Manipulation Through Representations, Self-Descriptive Messages, and HATEOAS. Code-On-Demand is marked optional, and almost every production API skips it without a second thought. The other five exist to buy the system something specific: scalability, simplicity, the ability to swap one part without breaking the rest, visibility into what's happening at each layer, portability, reliability. A constraint that doesn't earn one of those doesn't belong in the conversation.
What follows traces those constraints through an actual integration build: naming resources, shaping URIs, handling requests statelessly, picking the right HTTP method, reading a response correctly, and closing the security gaps that get left open when nobody's watching.
Why REST Became the Default API Architecture
REST sits at 93% adoption among developers, according to Postman's State of the API Report 2025, and nothing else is close. RapidAPI's 2024 Developer Survey puts REST at 83% of public APIs as of Q4 2024.
Near-universal adoption is running well ahead of near-universal understanding. Developers cite inconsistent documentation as their biggest roadblock, at 39% according to industry surveys, and a significant portion of that problem traces back to REST principles applied halfway or not at all. An API can call itself RESTful and still make its consumers guess how pagination works or what a 200 response actually promises.
Scale is where the sloppiness compounds. The average application now calls somewhere between 26 and 50 APIs, and companies managing their own footprint often juggle 50 or more across internal, partner, and public surfaces. Every bad pattern in one integration gets copied into the next. GraphQL is growing (33% adoption), so are WebSockets (35%), each one solving a problem REST handles poorly. Yet none of that dethrones REST; it stays the foundation everything else gets bolted onto. Getting it right once, in detail, is more valuable than getting it half-right across dozens of integrations.
Naming Resources and Shaping URIs Correctly
Before any code gets written, there's a naming problem to solve. The Identification of Resources sub-constraint draws a hard line: a URI names a thing, not an action. That's the actual boundary between REST thinking and RPC thinking, and it's the first place developers coming from an RPC background trip over their own habits.
Take a payment integration in the style of Stripe. The resources are customers, charges, payment methods, and refunds, and each one is a noun with its own URI. The wrong instinct is to write /createCharge or /getCustomer, verbs smuggled into a path where they don't belong. The right move is /charges and /customers/{id}, letting the HTTP method carry the verb instead.
Versioning has to get decided before the first endpoint ships, because retrofitting it later means breaking someone's live integration. There are three real options, and none of them is free:
- URI path versioning (
/v1/charges): explicit, testable in a browser, used by Stripe (api.stripe.com/v1/charges) and GitHub (api.github.com/repos/{owner}/{repo}). It technically breaks the rule that one URI names one unique resource, since/v1/charges/1and/v2/charges/1are arguably the same charge represented differently. - Header versioning (
X-Api-Version): keeps URIs clean, but invisible to anyone working with a browser or a plain curl command. - Query string versioning: trivial to build, and just as easily forgotten by client code that doesn't set the parameter.
Pick the failure mode you can survive, because a breaking change with no versioning plan breaks every client using the API today. URI path versioning wins for one reason: it's visible. A developer can see the version in the address bar, in the logs, and in the curl command a support engineer pastes into a ticket.
Stripe and Twilio are textbook cases of resource modeling done right. Twilio's key decision was treating phone calls and text messages as REST resources, which made the API feel predictable rather than clever.
Stateless Requests and What Clients Must Send
Fielding's own language is blunt: every request has to carry everything needed to understand it, and the server cannot rely on anything it remembers from a prior request, because session state lives on the client.
For someone building an integration, that requirement holds true on every single call without exception:
- Authentication credentials ride along with every request. There is no handshake that happens once and gets remembered later.
- Pagination state, whatever shape it takes, is the client's responsibility to track and resend every time.
- Context the server needs, such as filters, IDs, and permission scope, gets spelled out in the request itself, with nothing assumed.
Fielding acknowledged the cost of this himself: statelessness can hurt network performance, since the same data gets repeated across requests instead of sitting in shared server memory. He made that trade on purpose, in exchange for a system where any server can handle any request without needing to know what happened previously.
The benefit for an integration developer is concrete. Retry logic gets simpler, because a stateless request can be replayed without worrying it will corrupt some server-side record of what already happened. Idempotency becomes something you can reason about rather than hope for.
Two authentication patterns fit this model cleanly: API keys sent as a header or bearer token on every call, and OAuth 2.0 access tokens managed and refreshed on the client side. Both keep state exactly where REST requires it. Storing a session ID server-side and expecting later requests to reference it is standard practice in many web applications, but it is a direct departure from REST, and labeling it RESTful is where much of the confusion in the industry starts.
HTTP Methods and the Cost of Misusing Them
GET, POST, PUT, PATCH, DELETE: five methods, each one a promise about what happens when it is called, and breaking that promise means the damage spreads beyond the one request.
GET has to be safe and idempotent: it changes nothing, so it is safe to retry and safe to cache. POST creates something new and is not idempotent, meaning the same POST fired twice can create two duplicate resources. PUT replaces a resource wholesale and is idempotent, so firing it twice produces the same end state both times. Whether PATCH is idempotent depends entirely on how it is implemented, since it updates only part of a resource. DELETE removes something and should behave consistently even when called on something already gone.
The most common failure is using POST for everything out of RPC habit, or using GET for an operation that quietly changes state on the server. Both break the semantic contract that caches, proxies, and monitoring dashboards depend on. This is where the Layered System constraint becomes concrete: CDNs, gateways, and reverse proxies make real routing and caching decisions based on HTTP method alone. A POST standing in for a GET bypasses the caching layer the architecture was built around.
Idempotency keys address the one genuine gap that remains: what happens when a non-idempotent call, such as a POST that creates a charge, needs to survive a retry without creating a duplicate. One common solution is a client-generated idempotency key sent as a header. The server sees the key, recognizes the retry, and returns the original result instead of processing the request again. It works precisely because it does not ask the server to remember anything it was not explicitly told.
The resource /customers/{id} and its representation, whether JSON, XML, or whatever the Accept header requests, are two distinct things. Self-Descriptive Messages means the response tells the client what it is looking at via Content-Type, without requiring outside knowledge to parse it.
Reading Responses and Handling Errors Reliably
Status codes exist so machines don't have to read prose. A gateway, a monitoring tool, or a retry script should not need to parse a JSON body just to determine whether a request succeeded.
The 2xx range has real distinctions worth preserving: 200 OK, 201 Created, and 204 No Content each mean something different, and collapsing them all into a generic 200 discards information that downstream consumers rely on. The 4xx range tells the client what it did wrong: 400 for a malformed request, 401 for missing authentication, 403 for authentication that is present but insufficient, 404 for a resource that doesn't exist, and 429 for exceeding the rate limit. The 5xx range means the opposite: the client's request was valid, and the problem is on the server side.
The worst pattern still appearing in production APIs is returning HTTP 200 with an error buried inside the response body. It defeats the entire purpose of a standardized status code and confuses every tool built to trust that standard. The correct approach pairs the right status code with a response body that includes a machine-readable error code, a human-readable message, and, where knowable, which field caused the problem.
Pagination is a response design decision with real consequences. Offset-based pagination using a page number and a limit is simple to build but breaks down on large or fast-changing datasets, where rows shift between pages as data changes underneath the query. Cursor-based pagination points to an exact position in the list and remains stable even when surrounding data changes. A dataset with a few hundred rows can tolerate offset pagination indefinitely, but a dataset with millions cannot, and that gap is the source of many hard-to-reproduce production bugs.
Security Decisions That Must Happen at Design Time
REST endpoints sit exposed to the open internet by design, powering web apps, mobile clients, and third-party integrations. That exposure makes them a target, and deferring security to a later phase is how vulnerabilities get shipped.
OWASP's API Top 10 is the field's shared reference point for what tends to go wrong, and two entries map directly onto decisions covered in this walkthrough. Broken Object Level Authorization (BOLA) is what happens when an API lets an authenticated user access a resource that belongs to someone else. If /charges/{id} returns any charge to any authenticated caller, authorization is broken at the object level, and endpoint-level authentication checks do not fix it. The fix has to be built into the resource-identification decisions made at the URI design stage.
Authentication has mostly already been determined by the stateless constraint. API keys work for server-to-server integrations where the key never touches a browser. OAuth 2.0 with short-lived access tokens is appropriate when the integration acts on a user's behalf, with token refresh handled client-side, consistent with keeping state off the server. Neither pattern requires the server to remember a session.
Rate limiting, enforced through the 429 response, typically lives at the gateway layer, which is a responsibility the Layered System constraint appropriately assigns to infrastructure rather than application code. The integration's responsibility is handling that 429 with exponential backoff rather than immediately retrying. HTTPS is not optional in this context: REST's self-descriptive, human-readable format, plain JSON with bearer tokens in headers, exposes sensitive data directly if traffic is intercepted unencrypted.
What a Working Integration Validates and Leaves Open
A working integration built as this walkthrough describes fully satisfies Stateless Communication, Client-Server separation, and the resource-identification, representation, and self-description components of Uniform Interface.
Two additional constraints are satisfied, but not by the developer's own code. Layered System is handled by the gateway, CDN, and reverse proxy sitting in front of the integration. Cache is satisfied only if someone sets the correct HTTP cache headers; the mechanism exists in HTTP itself, but nothing forces a developer to use it.
Then there is HATEOAS, the constraint almost nobody implements. The idea is that a response should carry hypermedia links telling the client what it can do next, rather than having that logic baked into the client ahead of time through external documentation. Fielding has stated directly that APIs skipping this are not fully RESTful in the sense his dissertation describes, and he is correct, though the industry has largely moved on. The working definition of REST in production has become HTTP plus JSON plus resource-shaped URIs plus standard status codes, a useful subset of the original constraints, and a different thing from what Fielding described in 2000.
That subset still does real work. Statelessness removes ambiguity about what the server remembers. Correct status codes remove ambiguity about what a response means. Resource-oriented URIs remove ambiguity about what an operation is supposed to do. Each constraint, applied honestly rather than halfway, eliminates one category of uncertainty from the system.
What the constraints do not provide is a versioning strategy, a pagination style, or an error schema. REST is intentionally silent on all three. Those are engineering decisions filled in by convention and accumulated practice, not by the architecture itself. REST's durability at 93% adoption owes less to elegance or purity than to how cleanly its constraints map onto HTTP, and how cleanly HTTP maps onto how the internet already operates. Understanding the constraints in detail is what turns those implementation choices into deliberate decisions rather than habits inherited from the last API someone happened to read.



