Representational State Transfer (REST)

This piece examines the limitations and goals of the REST architectural style.

The Why Behind the How: Setting the Stage

Back in December 1990, Tim Berners-Lee envisioned a system for information exchange and thus began the World Wide Web project. Initially designed for a small group, the web’s popularity exploded upon public release, reaching 40 million users in just five years. This rapid growth strained the existing internet infrastructure, pushing software architects to devise a more robust web architecture—one that was flexible, high-performing, and could scale to handle ever-increasing traffic. The REST architectural style emerged as a solution, playing a crucial role in building scalable systems.

“It’s important to remember that decisions made at the architectural level should align with the functional, behavioral, and social needs of the system. This principle applies equally to both software architecture and traditional building architecture.” — Roy Fielding (Originator of REST)

REST: Taming the Web’s Growing Pains

REST, short for Representational State Transfer, came about in 2000, the brainchild of Roy Fielding. He recognized that scalability issues could be addressed by adhering to certain constraints, together forming the web’s architectural style and serving as the foundation for REST API design.

To handle the internet’s expansion, a practical approach was needed to address these constraints, which include:

  • Client-server
  • Cache
  • Stateless
  • Uniform interface
  • Layered System
  • Code-on-demand

Let’s break down each of these constraints:

Client-Server: Dividing and Conquering

The client-server model is a widely used architectural style for network-based applications. A server, hosting a range of services, listens for requests. Clients, on the other hand, use the internet as a connector to communicate with the server, sending requests and awaiting responses. The server either fulfills or rejects these requests, sending back a response to the client.

sequenceDiagram
    participant Client
    participant Server
    
    Client->>+Server: Send Request
    Note right of Server: Process Request
    Server-->>-Client: Send Response
    Note left of Client: Handle Response

    rect rgb(240, 240, 240)
        Note over Client,Server: Client-Server Communication Flow
    end

At the heart of the client-server constraint lies the principle of separation of concerns. To boost scalability, the server’s complexity is reduced by dividing tasks effectively. This often involves consolidating user interface elements into a dedicated client component. Both client and server can then evolve independently, as long as their communication interface remains consistent.

Cache: A Stash of Information

A cache, positioned between client and server, stores responses from previous requests. When a client sends a request, the cache checks if it has a matching response from an identical, earlier request. If so, the cached response is sent directly to the client, reducing the need to contact the server. This strategy enhances network efficiency and improves the user experience by speeding up response times.

Remember, response data can also be cached on the client-side for reuse with similar requests.

The cache constraint dictates that responses should be clearly marked as cacheable or non-cacheable. Cacheable responses allow the client’s cache to store and reuse them for similar requests.

However, the cache constraint has a tradeoff. It can potentially reduce reliability and consistency if the cached data becomes outdated and differs significantly from the data returned by a fresh request to the server.

Food for Thought: How can we ensure the data (or a webpage) stored in the cache is still valid?

Stateless: Living in the Moment

In a stateless system, the server does not retain any information about past requests. Each request from the client must contain all the information the server needs to process it, just like a vending machine doesn’t remember your previous purchases; it only cares about the selection and payment you provide right now. Remember, the server should not store any data from previous requests.

The stateless constraint brings several performance benefits:

  • Increased Reliability: The server can easily recover from crashes without data loss, as it’s not storing any client-specific information.
  • Enhanced Scalability: Since the server doesn’t store user data, resources are freed up immediately after processing a request, allowing it to handle a greater number of concurrent requests.
  • Improved Visibility: Because the server only considers the information within a single request, monitoring systems can easily understand the purpose of each request without needing additional context.

However, this constraint also has a tradeoff. Sending repetitive data in consecutive requests, due to the server’s inability to remember past interactions, can negatively impact network performance.

Uniform Interface: Speaking the Same Language

For a system composed of various parts to function effectively, consistent communication between these parts is essential. Applying the software engineering principle of generality to the components’ interfaces simplifies the overall system design and increases the transparency of interactions between these parts. Decoupling services from their implementation allows for the independent development of components.

Remember, data exchange between components should be standardized.

The downside of a uniform interface is a potential decrease in efficiency. Since information must be transferred in a standardized format, this might not always be the most optimal format for the application. However, if components don’t adhere to these standards, the web’s communication can break down.

graph TD
    subgraph Clients
        A[Web Browser]
        B[Mobile App]
        C[Desktop Application]
    end

    subgraph "Common Interface"
        D{API}
    end

    subgraph Server
        E[Server]
    end

    A -->|HTTP/HTTPS| D
    B -->|HTTP/HTTPS| D
    C -->|HTTP/HTTPS| D
    D <-->|Processes Requests/Responses| E

    classDef client fill:#f9f,stroke:#333,stroke-width:2px;
    classDef interface fill:#ff9,stroke:#333,stroke-width:2px;
    classDef server fill:#9ff,stroke:#333,stroke-width:2px;

    class A,B,C client;
    class D interface;
    class E server;

    style Clients fill:#f0f0f0,stroke:#333,stroke-width:2px;
    style Server fill:#f0f0f0,stroke:#333,stroke-width:2px;

To guide how components behave within this framework, additional architectural constraints are used:

  • Resource Identification: In REST, information is represented as resources. A resource can be any piece of information that can be named, such as a document, image, a collection of resources, or objects. Each resource is identified by a unique Uniform Resource Identifier (URI).
  • Resource Manipulation via Representations: A representation of data is a sequence of bytes and its associated metadata. The representation of resources can be manipulated by clients. Essentially, the same source of information can be presented in different formats to different clients. For example, a webpage can be rendered as HTML in a browser but displayed as XML or JSON in a console application.
  • Self-Descriptive Messages: To ensure a uniform interface, this constraint requires that messages, particularly responses, contain all the information a client needs to understand them. A status code like “200 OK” indicates success, and no additional documents or data should be needed to interpret this.
  • Hypermedia as the Engine of Application State (HATEOAS): Clients should be able to dynamically discover information through hypermedia. Hypermedia refers to interactive media that includes different content forms like text, images, videos, and hyperlinks. Resources requested from the server should include links to related resources, enabling users to navigate through information in a relevant and guided manner.

Layered System: Organizing Complexity

The layered system constraint introduces a structured hierarchy of components, known as layers, between the client and the server. This structure allows for the interception of client-server communication by intermediary components, which can simplify communication, enhance security, and ensure requests are routed correctly.

Key Point: Introducing additional components between the client and server can assist and direct their communication.

A layered approach improves decoupling. Each layer only interacts with the layers directly above and below it, hiding its internal workings from others.

flowchart TD
    A[Client] --> B[Proxy Server]
    B --> C[Load Balancer]
    C --> D[Security Layer]
    D --> E[Server 1]
    D --> F[Server 2]
    D --> G[Server 3]

    subgraph "Client Layer"
        A
    end

    subgraph "Proxy Layer"
        B
    end

    subgraph "Load Balancing Layer"
        C
    end

    subgraph "Security Layer"
        D
    end

    subgraph "Server Layer"
        E
        F
        G
    end

    classDef clientLayer fill:#f9f,stroke:#333,stroke-width:2px;
    classDef proxyLayer fill:#ff9,stroke:#333,stroke-width:2px;
    classDef loadBalancerLayer fill:#9ff,stroke:#333,stroke-width:2px;
    classDef securityLayer fill:#f96,stroke:#333,stroke-width:2px;
    classDef serverLayer fill:#9f9,stroke:#333,stroke-width:2px;

    class A clientLayer;
    class B proxyLayer;
    class C loadBalancerLayer;
    class D securityLayer;
    class E,F,G serverLayer;

This constraint makes the system more:

  • Evolvable: Layers can be modified or added without affecting others, as long as the interface between them remains consistent.
  • Reusable: Existing components can be reused, reducing the need to build everything from scratch.

One drawback is that requests might need to travel through more layers, potentially increasing latency.

Code-on-Demand: Extending Functionality

The code-on-demand constraint allows servers to send executable code (like plug-ins, scripts, or applets) to clients. This requires a degree of technological coupling, as the client application must be able to understand and execute the code received from the server. It is considered optional in the REST architectural style.

Key Point: Client applications can request executable code from servers to run locally.

sequenceDiagram
    participant Client
    participant Server
    
    Client->>+Server: Request code
    Server-->>-Client: Send executable code
    
    rect rgb(230, 255, 230)
    Note right of Client: Execute received code locally
    end
    
    Note over Client,Server: Code-on-Demand extends client functionality

    Client->>+Server: Request with extended functionality
    Server-->>-Client: Response

Code-on-demand offers:

  • Extensibility: Clients gain flexibility by having the ability to execute code, extending their functionality.
  • Performance Enhancements: Certain tasks can be offloaded to the client, improving performance and responsiveness.

One concern is that code from the server could be malicious. To mitigate this risk, a sandbox environment on the client-side is used to execute this code in isolation, preventing potential harm to the client system.

Another drawback is that monitoring systems might find it difficult to fully understand the nature of a request when code is involved.

Navigating the Constraints of REST

It’s important to remember that adhering to all constraints in REST might not always be possible. Some constraints can even contradict each other. For instance, while the stateless constraint increases visibility, using code-on-demand might reduce it.

 Here’s a table summarizing the features and drawbacks of each REST constraint:

REST ConstraintKey FeaturesPotential Drawbacks
Client-server– Improved scalability
– Enhanced evolvability of clients and servers
– Potential server overload with high request volume
– Service interruptions due to network issues
Cache– Reduced latency through client-side data storage– Reduced consistency if cached data becomes stale
Stateless– Increased reliability
– Enhanced scalability for requests
– Improved visibility of requests for analysis
– Reduced network performance due to repetitive data in consecutive requests
Uniform Interface– Improved independent component development
– Smoother interactions between entities
– Enhanced component evolvability
– Potential efficiency and flexibility limitations due to standardized data formats
Layered System– Improved component evolvability and reusability
– Enhanced decoupling of components
– Reduced performance due to processing overhead from multiple layers
– Increased security needs for each layer
Code-on-Demand– Improved extensibility
– Increased performance
– Reduced request trackability by monitoring systems
– Potential client-side security risks if code is malicious