DEV Community

Kishi
Kishi

Posted on

REST Isn’t Just About JSON Over HTTP

When most developers talk about building a REST API, what they usually mean is sending JSON over HTTP with a set of CRUD endpoints like /users or /orders. That approach works well in practice, which is why it’s so common. But according to Roy Fielding, the person who introduced the concept of REST in his doctoral dissertation, this isn’t really REST at all. That pattern has become so popular that “REST API” is almost shorthand for “any JSON API.”

REST stands for Representational State Transfer, and its principles go beyond HTTP verbs and resource URLs. Fielding emphasized one idea in particular that often gets overlooked: hypermedia. His vision was that clients should be able to discover what actions are possible by following links included in the server’s responses, rather than hardcoding every endpoint path ahead of time. This principle is called HATEOAS (Hypermedia as the Engine of Application State).

Fielding went so far as to say that most of what the industry calls REST APIs today are not RESTful. They are REST-like.

So what did Fielding actually mean? Let’s unpack it.


The Six Constraints of REST

Fielding described REST as an architectural style defined by a set of constraints. If an API follows all of them, it can be called RESTful. If it ignores some, it might still be useful, but it isn’t REST.

Client–Server Architecture

At its core, the client–server architecture is about separating concerns. In a RESTful system, the client is responsible for the user interface and user experience, while the server handles data storage, business logic, and processing. This separation allows both sides to evolve independently. For example, you could redesign your web frontend or switch from a web app to a mobile app without touching the server, as long as the API contract remains consistent.

This separation also encourages scalability. Servers can focus purely on efficiently processing requests and managing resources, while clients handle how data is presented and interacted with. In practical terms, a request like GET /users/1 is handled entirely by the server, which retrieves the user data from a database and formats it into a response. The client doesn’t need to know how the data is stored or computed. It only needs to know how to interpret and display the response.

Another advantage is independent deployment. Because the client and server are decoupled, teams can release new versions of one without requiring changes in the other. This flexibility reduces coordination overhead and allows for faster iteration.

Finally, the client–server separation lays the groundwork for interoperability. Multiple types of clients -- web browsers, mobile apps, IoT devices -- can interact with the same server using the same API. The server doesn’t care whether the request comes from a browser running JavaScript, a Python script, or a mobile app; it responds the same way, serving data consistently.

Statelessness

Statelessness is one of the defining constraints of REST, and it has a profound impact on how APIs are designed and scaled. In a stateless system, every request from a client must contain all the information necessary for the server to understand and process it. The server does not store any client session or context between requests. Each interaction is independent, meaning the server treats it as a completely new request.

For example, imagine a client wants to fetch the details of a user:

GET /users/123
Authorization: Bearer abc123token
Enter fullscreen mode Exit fullscreen mode

Here, the request includes everything the server needs: the endpoint identifies the resource, and the authorization header provides the credentials. The server doesn’t need to “remember” anything about the client’s previous requests. If the client sends the next request to fetch the user’s orders, it must include all necessary context again—perhaps the same authentication token or other parameters.

This design brings several practical benefits. First, it makes scaling much easier. Because servers do not store session information, any server in a cluster can handle any incoming request. There’s no need to tie a client to a specific server, which simplifies load balancing and improves fault tolerance.

Second, statelessness increases reliability. If one server goes down, another can seamlessly handle the next request. There’s no risk of losing session state or requiring complicated session replication mechanisms.

Finally, statelessness encourages clearer API design. Clients must explicitly provide all necessary information, which forces developers to think carefully about what data is required and how to structure requests. While this may seem repetitive, it leads to more predictable and maintainable systems.

Cacheability

Cacheability is a fundamental REST constraint that directly impacts performance, scalability, and efficiency. In a RESTful system, responses must clearly indicate whether they can be cached and for how long. By providing explicit cache control information, servers enable clients, and even intermediate proxies, to reuse responses without sending repetitive requests. This reduces server load, improves response times, and makes the system more scalable.

For instance, imagine a client requests product information from an API:

GET /products/123
Cache-Control: max-age=3600

Enter fullscreen mode Exit fullscreen mode

This response tells the client that the data can be cached for one hour. If the same request is made again within that hour, the client can use the cached response instead of querying the server again. This reduces unnecessary traffic and speeds up user interactions.

Cacheability also improves network efficiency in larger architectures. In layered systems with proxies, CDNs, or load balancers, properly cacheable responses allow these intermediaries to serve repeated requests without hitting the origin server. For example, a static product catalog or frequently accessed user profile data can be cached at multiple layers, decreasing latency and conserving server resources.

It’s important to note that not all responses are safe to cache. Dynamic or sensitive data, such as user-specific transactions or authentication details, must explicitly prevent caching with headers like Cache-Control: no-store. Making this distinction ensures data integrity and security while still benefiting from caching where appropriate.

Uniform Interface

The Uniform Interface constraint is the cornerstone of REST and arguably the most important of all. Fielding designed REST around the idea that a consistent, standardized interface between clients and servers simplifies interactions, improves scalability, and reduces coupling. In other words, every client can interact with any server in a predictable way without needing intimate knowledge of its internal implementation.

The uniform interface is built around four key principles. First, resource identification in requests. Every resource is uniquely addressable through a URI. For example, /users/1 unambiguously refers to a specific user. This consistent identification allows clients to locate resources without guessing endpoints.

Second, manipulation through representations. Clients do not directly manipulate resources on the server. Instead, they work with representations -- like JSON or XML -- that capture the current state of a resource. If you want to update a user’s profile, you send a JSON representation of the new state to the server, and the server interprets it to apply the changes. This separation keeps clients decoupled from the server’s internal data structures.

Third, self-descriptive messages. Each request and response should contain enough information to describe how it should be processed. HTTP verbs (GET, POST, PUT, DELETE) communicate the intent of a request, while headers and payloads provide additional context. For example, sending a PUT request with a JSON payload to /users/1 clearly indicates an update operation. The client does not need to embed hidden instructions or out-of-band knowledge.

Finally, and perhaps most overlooked, is Hypermedia as the Engine of Application State (HATEOAS). Fielding envisioned APIs where clients discover available actions by following links embedded in resource representations, rather than hardcoding endpoint URLs. For example, a user object might include links to their orders:


{
  "id": 1,
  "name": "Alice",
  "_links": {
    "self": { "href": "/users/1" },
    "orders": { "href": "/users/1/orders" }
  }
}
Enter fullscreen mode Exit fullscreen mode

The client doesn’t need prior knowledge of the orders endpoint; it can simply follow the link provided by the server. This makes APIs discoverable, evolvable, and resilient to changes in URL structures.

Hypermedia as the Engine of Application State (HATEOAS)

Hypermedia is arguably the most defining, yet most overlooked, constraint of REST. Fielding designed REST around the idea that clients should be able to navigate the application solely by following links provided dynamically by the server, without needing prior knowledge of the API structure. This principle is known as HATEOASHypermedia as the Engine of Application State.

In most APIs labeled “RESTful” today, clients hardcode the paths to endpoints: /users/1/orders, /products/42/reviews, and so on. While this works, it violates the core idea of REST. In a truly RESTful system, the client discovers these paths dynamically by following links embedded in the server’s responses. The server drives the application state, guiding the client through available actions.

For example, consider a user resource returned by an API:

{
  "id": 1,
  "name": "Alice",
  "_links": {
    "self": { "href": "/users/1" },
    "orders": { "href": "/users/1/orders" },
    "update": { "href": "/users/1", "method": "PUT" }
  }
}

Enter fullscreen mode Exit fullscreen mode

The _links section tells the client what it can do next: retrieve orders, update the user, or simply follow the self-link. The client doesn’t need to guess endpoint URLs or rely on external documentation for navigation. If the server later reorganizes its endpoints, the client can still function as long as it continues to follow the provided links.

This design has several major advantages. First, it makes APIs evolvable. Servers can change URL structures or introduce new features without breaking existing clients. Second, it enhances discoverability. Clients can explore an API like a web browser explores the web, learning available actions dynamically. Finally, it enforces a stronger separation of concerns: the server controls application logic and available actions, while the client focuses on user interaction and presentation.

Despite these benefits, hypermedia is rarely implemented in practice. Most developers consider it “too complex” or unnecessary when simple JSON endpoints suffice. But the reality is that hypermedia is what makes REST truly RESTful. Without it, an API is just HTTP + JSON masquerading as REST. HATEOAS ensures that clients remain decoupled from server implementations while still being able to navigate the system efficiently.

Layered System

The Layered System constraint in REST allows an architecture to be composed of hierarchical layers, each with a specific role, without the client needing to know how many layers exist or what each layer does. In other words, a client interacts with the server as if it were a single, unified system, even if requests pass through multiple intermediaries such as proxies, gateways, or load balancers.

The key idea is that clients are agnostic to the system’s internal structure. They don’t need to know how many layers exist or what each does. They only care about receiving consistent responses. By enforcing this separation, RESTful architectures become more robust, scalable, and maintainable, especially in distributed systems.

Code-on-Demand (Optional)

The last of Fielding’s REST constraints is Code-on-Demand, and it is the only one that is optional. This constraint allows servers to extend client functionality by sending executable code that the client can use dynamically. In other words, the server can temporarily enhance or customize the client’s behavior without requiring a new client release.

A classic example is web browsers. When you visit a webpage, the server sends HTML, CSS, and JavaScript. The JavaScript is executable code that runs on the client side, enabling interactive features like dynamic forms, animations, or API calls. In REST terms, the server is providing code-on-demand, allowing the client to adapt its behavior on the fly.

In API contexts, code-on-demand could be used more subtly. For instance, a server might send a small script that validates input, performs calculations, or handles specific business logic before sending data back. This allows the client to stay lightweight while leveraging server-provided functionality.

While not widely used in most APIs, code-on-demand adds flexibility and extensibility. Clients can remain generic and simple, while servers can provide the additional logic needed for specific operations. It also supports the principle of loose coupling: clients do not need to be updated every time new functionality is added to the system—they simply receive the code they need from the server.

However, because it introduces executable code on the client, code-on-demand must be used carefully to avoid security risks or unintended side effects. That’s partly why it is optional and less commonly implemented in standard APIs.

Top comments (0)