API Advice
Designing your API: Find the RESTful sweet spot
Sagar Batchu
January 1, 2025
API Design Guide
If you’re looking for a more comprehensive guide to API design, you can read our REST API Design Guide.
What we call RESTful APIs today often refer to JSON over HTTP, which is an offspring of the RESTful APIs Roy Fielding defined in his dissertation (opens in a new tab) in 2000. Back then, JSON was still just an idea, and the web was still in its infancy. The constraints Fielding defined in his dissertation were a reaction to the state of the web at the time, but they remain relevant to the web today. Rather than co-opting the term RESTful, JSON-over-HTTP APIs could have benefited from a new term that acknowledges their differences from Fielding’s REST.
Alas, the term RESTful stuck, and here we are. This article will explore what it means to be RESTful in 2025, why it matters, and how to find the sweet spot between adhering to RESTful principles and following established practices.
First, let’s clarify the difference between RESTful APIs and REST-like APIs.
Designing your API: Find the RESTful sweet spot
Fielding’s original REST model defines six architectural constraints, liberally summarized as follows:
- Client-server architecture: This separation allows the client and server to evolve independently as long as the interface doesn’t change.
- Statelessness: Each request from the client contains all the information the server needs to fulfill that request, easing server workload and improving scalability.
- Cacheability: Responses are explicitly labeled as cacheable or non-cacheable, which helps reduce client-server interactions and improves performance.
- Uniform interface: This constraint is broken into four subconstraints.
- Resource identification: Resources are identified in requests, typically using URIs.
- Resource manipulation through representations: Resources are manipulated through representations, such as JSON or XML.
- Self-descriptive messages: Messages include all the information needed to understand them.
- Hypermedia as the engine of application state (HATEOAS): Responses include links to related resources, allowing clients to navigate the API dynamically.
- Layered system: A client should be unable to tell whether it is connected directly to the end server or to an intermediary along the way.
- Code on demand (optional): Servers can extend client functionality by transferring executable code, like Java applets or client-side scripts.
Adherence to these constraints distinguishes a truly RESTful API from one that is REST-like.
How most REST-like APIs adhere to REST constraints
If you’re building a modern API, you’re likely adhering to some of the REST model’s constraints, even if you’re not following them all to the letter. The key is to understand the principles behind REST and apply them in a way that makes sense for your use case.
✅ Client-server architecture: Followed by most REST-like APIs
In the context of APIs, this means that the client and server communicate over a network, with the server providing resources and the client consuming them. This separation is central to RESTful design and may be why the term “RESTful” was adopted for APIs that follow this pattern.
✅ Statelessness: Followed by most REST-like APIs
Statelessness means that each request from the client to the server must contain all the information needed to fulfill that request. This constraint simplifies server logic and improves scalability by allowing servers to handle requests independently.
Most APIs follow this constraint by requiring clients to include all necessary information in each request.
✅ Cacheability: Followed by most REST-like APIs
Cacheability allows responses to be explicitly labeled as cacheable or non-cacheable, reducing the need for repeated requests to the server. By specifying cacheability, APIs can improve performance and reduce server load.
Most APIs follow this constraint by including cache-control headers in their responses.
The first request retrieves the order from the server and caches it. The subsequent request is served from the cache, reducing the load on the server.
⚠️ Uniform interface: Partially followed by most REST-like APIs
The uniform interface constraint is seen by many as the heart of REST. It defines a standard way to interact with resources, making APIs more discoverable and easier to use. This constraint is often broken down into four sub-constraints:
✅ Resource identification: Resources are identified in requests, typically using URIs. Followed by most APIs.
✅ Resource manipulation through representations: Resources are manipulated through representations, such as JSON or XML. Followed by most APIs.
⚠️ Self-descriptive messages: Messages include all the information needed to understand them. Through the use of media types, APIs can achieve this sub-constraint. Partially followed by most APIs.
❌ Hypermedia as the engine of application state (HATEOAS): Responses include links to related resources, allowing clients to navigate the API dynamically. Rarely followed by APIs.
The last two sub-constraints of uniform interfaces are often the most challenging to implement and are frequently omitted in practice.
HATEOAS, in particular, is a powerful concept that applies extremely well to web APIs that serve human users. For example, HTML returned by a web server contains links that users can click to navigate the web.
Take this HTML response as an example:
curl --header "Accept: text/html" https://api.example.com/orders/123
<!doctype html><html><head><title>Order 123</title></head><body><h1>Order 123</h1><p>Status: Shipped</p><a href="/orders/123">View Order</a><a href="/customers/456">View Customer</a></body></html>
In this case, the links are clickable, allowing users to navigate the API by following them. This is the essence of HATEOAS.
Contrast this with a JSON response:
curl --header "Accept: application/json" https://api.example.com/orders/123
{"id": 123,"status": "shipped","links": [{ "rel": "self", "href": "/orders/123" },{ "rel": "customer", "href": "/customers/456" }]}
In this example, the response includes links to the order itself and the customer who placed the order. By following these links, a client can navigate the API without prior knowledge of its structure. In practice, an SDK or client library would need to be aware of these links to provide a similar experience. From a developer experience perspective, this can be challenging to implement and maintain.
Instead of implementing HATEOAS, many APIs rely on documentation to inform developers how to interact with the API. While this approach is more common, it lacks the dynamic nature of HATEOAS.
✅ Layered system: Followed by most REST-like APIs
The layered system constraint allows for intermediaries between the client and server, such as proxies or gateways. This separation enhances scalability and security by isolating components and simplifying communication.
Most APIs follow this constraint by permitting intermediaries between the client and server.
Since API requests are stateless and contain all the information needed to fulfill them, intermediaries can forward requests without needing to maintain session state. This is especially useful for load balancing and security purposes.
⚠️ Resource-oriented architecture: Followed by some REST-like APIs
Each of the constraints above contributes to a resource-oriented architecture, where resources are identified by URIs and manipulated through representations. This architecture makes APIs more predictable and easier to use by following standard resource interaction patterns.
Most APIs follow this constraint by organizing their resources into collections and items, with standard methods for interacting with them.
This pattern is often reflected in an API’s URL structure, where resources are presented as nouns, and instead of using verbs in the URL, API actions are represented by HTTP methods. For example:
GET /orders
: Retrieve a list of orders.POST /orders
: Create a new order.GET /orders/123
: Retrieve order 123.PUT /orders/123
: Update order 123.DELETE /orders/123
: Delete order 123.
Many API design guidelines recommend using resource-oriented URLs to make APIs more intuitive and easier to use. We’ll explore this in more detail later in the article.
❌ Code on demand: Rarely used in the context of APIs
We’ll skip this constraint for now, as it’s optional and rarely implemented in practice outside hypermedia-driven APIs.
Why adherence matters
Is this a cargo cult, or do these constraints actually matter? They do, and here’s why:
The principle of least astonishment
There is a big difference between delighting users and surprising them. The principle of least astonishment states that the behavior of a system should be predictable and consistent.
When your API behaves in a predictable way, like sticking to the standard ways of using HTTP methods and formats, you’re making it easy for developers to use. They shouldn’t have to spend hours figuring out quirky behaviors or unexpected responses - that just leads to headaches and wasted time.
Scalability
Scalability is essential when designing APIs that need to handle varying loads and growth over time. Adhering to REST principles inherently supports scalability in several ways:
- Statelessness: Without the need to maintain session state, servers can handle requests independently, making it easier to scale horizontally.
- Cacheability: REST APIs explicitly label responses as cacheable. This reduces the server load, as cached responses can be reused from previous requests.
- Layered system: REST architecture allows the deployment of intermediary servers, such as load balancers and cache servers, which isolate client requests from direct backend processing.
Maintainability
The constraints of the REST model naturally lead to a design that is easier to update and manage:
- Resource-oriented design: By focusing on resources rather than actions, APIs become more modular and logically structured.
- Independent client and server evolution: The client-server separation supported by REST allows both sides to evolve independently.
Quantifying REST adherence
The Richardson Maturity Model (opens in a new tab) provides a framework for evaluating an API’s compliance with RESTful principles. This model outlines four levels of RESTfulness, each building on the previous one, allowing you to assess and improve your API’s design objectively:
Level 0: The swamp of POX (Plain Old XML)
At this base level, APIs typically rely on a single URI and use HTTP merely as a transport protocol. There is little to no differentiation in the use of HTTP methods, and operations tend to be defined solely by the payload. This resembles the remote procedure call over HTTP (RPC over HTTP) protocol, where the rich set of HTTP features, like methods and status codes, are underutilized.
An example of a Level 0 API might be:
curl -X POST https://api.example.com/api.aspx?method=createOrder -d "..."
Level 1: Resources
The first step towards RESTfulness is exposing resources via distinct URIs. At Level 1, APIs start organizing data into resources, often with a collection-item structure, making the API more resource-oriented. Each resource, such as /orders
or /customers
, typically represents a collection of items, with individual resources accessible by identifiers like /orders/{orderId}
.
An example of a Level 1 API might be:
# Retrieve a list of orderscurl -X POST https://api.example.com/orders# Retrieve order 123curl -X POST https://api.example.com/orders/123
Note how the API is starting to use URIs to represent resources, but the use of POST for retrieval is not ideal.
Level 2: HTTP verbs
At this level, RESTful APIs progress by using HTTP methods (like GET, POST, PUT, and DELETE) to perform operations on resources. Level 2 APIs respect the semantics of these verbs, leveraging the full power of HTTP to perform operations that are predictable and standardized. For example, GET retrieves data, POST creates new resources, PUT updates existing resources, and DELETE removes them.
An example of a Level 2 API might be:
# Retrieve a list of orderscurl -X GET https://api.example.com/orders# Retrieve order 123curl -X GET https://api.example.com/orders/123# Create a new ordercurl -X POST https://api.example.com/orders -d "..."# Update order 123curl -X PUT https://api.example.com/orders/123 -d "..."# Delete order 123curl -X DELETE https://api.example.com/orders/123
Level 3: Hypermedia controls (HATEOAS)
HATEOAS distinguishes Level 3 APIs from the rest. At Level 3, responses include hypermedia links that offer clients dynamic navigation paths within the API. This allows for discoverability directly embedded in API responses, fostering a dynamic interaction model in which clients can follow links to escalate through states or access related resources.
Evaluating your API’s RESTfulness
To evaluate the RESTfulness of your API, consider the following questions:
-
Level 1: Are resources uniquely identified by URIs?
For example,
/orders/123
uniquely identifies an order resource. -
Level 2: Are all URLs resource-oriented, and do they use standard HTTP methods?
For example, URLs should use GET to retrieve resources, POST to create new resources, PUT to update existing resources, and DELETE to remove resources.
None of your endpoint URLs should contain verbs or actions. For example, neither
/cancelOrder
nor/orders/123/cancel
is resource-oriented,. -
Level 3: Do responses include hypermedia links for dynamic navigation?
For example, a response might include links to related resources, allowing clients to navigate the API without prior knowledge of its structure.
The RESTful sweet spot
We believe the RESTful sweet spot lies somewhere between Level 1 and Level 2 of the Richardson Maturity Model. This is where most APIs can find a balance between adhering to RESTful principles and practicality.
In case it wasn’t crystal clear from the previous sections, we think HATEOAS isn’t practical or relevant for most APIs. It was a powerful concept when the web was young, and APIs were meant to be consumed by humans.
Here’s what we recommend:
-
Embrace resource-oriented design: Think in terms of resources rather than actions. Identify the key resources in your API domain and structure your endpoints around them.
This ensures your API is predictable and intuitive, making it easier for developers to understand and use.
✅ Good: Use resource-oriented URLs like
/orders
and/customers
.openapi.yamlpaths:/orders: # Resource-orientedget:summary: Retrieve a list of orderspost:summary: Create a new order/orders/{orderId}: # Resource-orientedget:summary: Retrieve order detailspatch:summary: Update an orderput:summary: Replace an orderdelete:summary: Delete an order❌ Bad: Don’t use action-based URLs like
/cancelOrder
.openapi.yamlpaths:/cancelOrder: # Not resource-orientedpost:summary: Cancel an order✅ Compromise: Use sub-resources like
/orders/{orderId}/cancellations
.If you must include actions in your URLs, consider using sub-resources to represent them as resources in their own right.
openapi.yamlpaths:/orders/{orderId}/cancellations: # Resource-orientedpost:summary: Create a cancellation for an order✅ Less ideal compromise: Use top-level actions like
/orders/{orderId}/cancel
.openapi.yamlpaths:/orders/{orderId}/cancel: # Resource-oriented with actionpost:summary: Cancel an order -
Use standard HTTP methods wisely: Choose the appropriate method for each operation - GET for retrieval, POST for creation, PATCH for partial updates, PUT for complete updates or replacement, and DELETE for removal. Ensure your API follows the semantics of these methods.
This allows developers to predict how operations will behave based on the HTTP method. It also implies idempotency, safety, and cacheability where applicable.
An operation is considered safe if it doesn’t modify resources. It is idempotent if the result of performing it multiple times is the same as performing it once. It is cacheable if the response can be stored and reused.
openapi.yamlpaths:/orders:get: # Safe, idempotent, cacheablesummary: Retrieve a list of orderspost: # Unsafe, potentially idempotent, not cacheablesummary: Create a new order/orders/{orderId}:get: # safe, idempotent, cacheablesummary: Retrieve order detailspatch: # unsafe, potentially idempotent, not cacheablesummary: Update an orderput: # unsafe, idempotent, not cacheablesummary: Replace an orderdelete: # unsafe, idempotent, not cacheablesummary: Delete an order -
Document thoroughly with OpenAPI: Instead of relying on HATEOAS for dynamic navigation, use OpenAPI to provide comprehensive documentation for your API. OpenAPI allows you to define your API’s structure, endpoints, methods, parameters, and responses in a machine-readable format. This ensures clarity and type safety for developers using your API.
How targeting SDK generation enables better API design
While you’re designing your API, consider how it will be consumed by developers. If you’re providing an SDK or client library, you can optimize your API design to make SDK generation easier and more effective, and you may find the optimized design also leads to a more RESTful API.
We often see APIs with action-based URLs like /orders/{orderId}/cancel
or /orders/{orderId}/refund
. While partially resource-oriented, these URLs include actions as part of the URL.
Firstly, these URLs are not as maintainable as resource-oriented URLs. If you decide to allow multiple cancellations for an order - for example, when an order is restored after being canceled and then canceled again - you may wish to represent cancellations as resources in their own right. This would lead to URLs like /orders/{orderId}/cancellations/{cancellationId}
. Alternatively, you may wish to allow partial refunds, leading to URLs like /orders/{orderId}/refunds/{refundId}
.
Secondly, these URLs are not as predictable as resource-oriented URLs. Developers may not know which actions are available for a given resource, leading to a reliance on documentation or trial and error.
From a design perspective, this could look like the following:
paths:/orders/{orderId}/cancellations:post:summary: Set the order status to cancelled/orders/{orderId}/refunds:post:summary: Create a refund for the orderrequestBody:content:application/json:schema:type: objectproperties:amount:type: numberminimum: 0maximum: 100
Then, in a future version of your API, you could introduce cancellations and refunds as resources in their own right:
paths:/orders/{orderId}/cancellations:get:summary: Retrieve a list of cancellations for the orderpost:summary: Create a new cancellation for the order/orders/{orderId}/cancellations/{cancellationId}:get:summary: Retrieve a specific cancellation for the order/orders/{orderId}/refunds:get:summary: Retrieve a list of refunds for the orderpost:summary: Create a new refund for the order/orders/{orderId}/refunds/{refundId}:get:summary: Retrieve a specific refund for the order
This approach allows you to evolve your API over time without breaking existing clients.
Going beyond RESTful principles
While adhering to RESTful principles is essential, it’s also important to consider the practicalities of API design. Here are some detailed topics we’ll explore in future articles:
- Pagination: How to handle large collections of resources. Should you use offset-based pagination, cursor-based pagination, or something else?
- Filtering and searching: How to allow clients to filter and search resources efficiently.
- Error handling: How to communicate errors effectively and consistently.
- Versioning: How to version your API while maintaining backward compatibility.
- Security: How to secure your API using authentication and authorization mechanisms.
- Rate limiting: How to protect your API from abuse by limiting the number of requests clients can make.
- Webhooks: How to implement webhooks for real-time notifications.
Be sure to check back for more insights on API design.