I am sorry, but you do not know what a RESTful API is. I know you are a seasoned developer who has integrated APIs of all kinds, and you may even have dozens in production across multiple languages. But all of that only proves you have followed the industry's good practices and read immeasurable lines of documentation, which is good. However, none of it certifies your knowledge of RESTful. Don't panic! You are not alone. Most developers I know could not define it, implement it or point out its strengths either. That is why in this article I am going to explain its virtues with examples that are easy to understand. And you may well never look at an API the same way again once you finish reading. You have been warned!

API RESTful is often used as a synonym for REST API (Representational State Transfer), and the nuance matters. REST is not an HTTP interface: it is an architectural style, defined by Roy Fielding in his doctoral dissertation (2000), that imposes constraints:

  • Client-server architecture: The client and the server must be separated.
  • Stateless: Each client request to the server must contain all the information needed to understand and process the request. In other words, the server will not keep information about the client's state between requests.
  • Cacheable: Responses must be explicitly marked as cacheable or non-cacheable.
  • Layered system: The architecture can be composed of layers, where each layer has a specific function and they are isolated from each other.
  • Uniform interface: Communication between the client and the server must be predictable, with a well-defined pattern.
  • Code on demand (optional): The server can send executable code to the client, such as JavaScript scripts, to extend the client's functionality.

In other words, REST is a set of architectural principles for serving resources, usually over HTTP. It is so ingrained in web development, and so standardized, that it feels strange when we do not see it.

And here comes the uncomfortable part: strictly speaking, REST already includes everything you are about to read in this article. Hypermedia is part of the uniform interface constraint, and "RESTful" is simply the adjective, "conforming to REST". What has happened is that popular usage has been degrading "REST" until it means "HTTP API that returns JSON", so much so that Fielding himself published a famous article in 2008, REST APIs must be hypertext-driven, complaining that we call REST any RPC dressed up in HTTP. The Richardson Maturity Model put numbers on that distance: level 0, a single RPC-style endpoint; level 1, resources with their own URI; level 2, HTTP verbs and status codes used properly; level 3, hypermedia. The vast majority of the "REST" APIs you consume every day stay at level 2.

In this article I will use RESTful to refer to that level 3, the one Fielding demands. The goal is to turn an API into a predictable, standardized and self-descriptive interface. With just the base URL, the client will be able to explore every resource, without resorting to external documentation. In addition, we will have the flexibility to change routes without affecting the client, or even play with several communication protocols.

But first there is a concept you must know: HATEOAS (Hypermedia as the Engine of Application State). This concept is fundamental to understanding how a RESTful API can be self-descriptive and navigable.

HATEOAS (Hypermedia as the Engine of Application State)

Hypermedia is one of the key characteristics of RESTful: it allows clients to dynamically discover resources through links provided in the responses. They usually live under the _links parent.

{ "id": 123, "name": "John Doe", "_links": { "self": { "href": "/users/123", "method": "GET" }, "update": { "href": "/users/123", "method": "PUT" }, "delete": { "href": "/users/123", "method": "DELETE" }, "friends": { "href": "/users/123/friends", "method": "GET" }, "posts": { "href": "/users/123/posts", "method": "GET" }, "search": { "href": "/search/?query={query}", "method": "GET", "templated": true } } }
Thanks to this, clients can navigate the API by parsing and following the links to reach the information they need, similar to how you would browse the internet. Furthermore, since you jump between relative routes (href) using their names as identifiers (the object key), the backend could change the addresses without affecting the client. It is very powerful because the client is not tied to a fixed route structure; it moves between nodes.

Notice the use of templated: true in the last link. It indicates that the href contains a URI template (following RFC 6570) that must be filled in with specific values before use. It is an elegant way to discover parameterized endpoints. For example, the client could replace {query} with "restful api" to search for related content.

An honest note before we continue: the _links convention comes from HAL (Hypertext Application Language), but HAL does not define the method field; its links only contemplate properties such as href, templated, type or title. I add it because in practice it helps the API be self-descriptive, and it is a common extension. If you need actions with formalized methods and fields, look at Siren; if you prefer pure HAL, omit method and trust the protocol's conventions (rule 2).

Not every resource needs hypermedia, only the relevant ones with the right context. For example, it would make no sense to include links to a shopping cart in a blog article, but it would be interesting for an article to include the link to the author, the comments or related articles.

Now that we understand the importance of HATEOAS, let's look at the rules a RESTful API must follow.

The 6 rules of a RESTful API

I am not making these rules up: they are the six conditions Fielding lists in the article I mentioned earlier, adapted here with examples.

1. Don't depend on a single protocol

Use resource identifiers (URIs) to define resources. In other words, instead of using a URL (http://example.com/api/users/123), use a URI that indicates its location and ignores the protocol (/users/123). This way you could use others such as WebSocket, MQTT, NNTP, RPC, etc. Of course you can use HTTP, but you must not depend only on it. For example, if your API is designed to work exclusively over HTTP, it would not be strictly RESTful.

2. Don't change the protocol

Don't reinvent the wheel. Don't get creative with protocols. For example, if you use HTTP, follow the conventions: GET to fetch resources, POST to create, PUT to replace, PATCH for partial updates and DELETE to remove. Use the proper HTTP response statuses: 200 OK, 201 Created, 204 No Content, 400 Bad Request, 404 Not Found, etc. Using MQTT? Use the proper commands and statuses.

3. Focus on media types, not URIs

Instead of documenting each URI externally, you must make your API describe the media types it handles. It is the most misunderstood rule of the six, and for Fielding it is where almost all the descriptive effort should go. The idea: instead of publishing a list of routes (what we usually call "the documentation"), you define and document your media types (for example application/vnd.myshop.product+json), that is, what the fields of each representation mean and how its links are processed. The client decides what to do based on the Content-Type it receives, not on the URI it called. URIs become interchangeable details.

For example, the resource for a user with little information could be:

{ "id": 123, "name": "John Doe", "_links": { "self": "/users/123", "friends": "/users/123/friends", "lastInvoice": "/users/123/invoice/last" } }
While the resource for a user with more information could be:

{ "id": 123, "name": "John Doe", "_links": { "self": { "href": "/users/123", "method": "GET", "type": "application/json" }, "friends": { "href": "/users/123/friends", "method": "GET", "type": "text/csv" }, "lastInvoice": { "href": "/users/123/invoice/last", "method": "GET", "type": "application/pdf" } } }

4. Don't assume URI structures

You must not store or reuse URI structures in the client. An API could change them without notice, and your client would obviously stop working. Instead, parse the links and follow the relative identifiers (rel).

A resource can have the following structure:

{ "id": 987, "name": "Totoro plush", "price": 19.99, "_links": { "self": { "href": "/products/987", "method": "GET" }, "addToCart": { "href": "/cart/add/987", "method": "POST" }, "reviews": { "href": "/products/987/reviews", "method": "GET" } } }
And the next day:

{ "id": 987, "name": "Totoro plush", "price": 19.99, "_links": { "self": { "href": "/shop/item/987", "method": "GET" }, "addToCart": { "href": "/shop/cart/987", "method": "POST" }, "reviews": { "href": "/shop/item/987/reviews", "method": "GET" } } }

5. Avoid resource "types"

Don't expose the hierarchy, permissions or types of resources in the API. The client neither cares nor should know. For example, you should not have a /users/admin/123 or /users/guest/123 resource. Instead, use the links to define the actions that can be performed on the resource.

6. Start with a bookmark (root resource) and let the client explore

Clients must start with a root URL (or bookmark), and from there they will navigate your API-verse.

{ "message": "Welcome to my RESTful API", "_links": { "users": { "href": "/users", "method": "GET" }, "products": { "href": "/products", "method": "GET" }, "orders": { "href": "/orders", "method": "GET" }, "search": { "href": "/search/?query={query}&page={page}", "method": "GET", "templated": true }, "user-by-id": { "href": "/users/{user_id}", "method": "GET", "templated": true }, "product-search": { "href": "/products/search/?name={name}&category={category}", "method": "GET", "templated": true } } }
Notice that several links use templated: true, the URI templates we saw in the HATEOAS section: the client discovers even the parameterized endpoints without leaving the response.

You now have a general idea of how to build a RESTful API. Between the REST principles, the RESTful API rules and the HATEOAS concept, you will be able to build a solid API.

FAQ

How do I indicate the version of the API or endpoint?

The most common approach is to include the version in the URL, for example: /api/v1/users. However, if you want to follow the RESTful rules, you should take the version out of the URI. The most recommended option is to negotiate it with the Accept header using the structure application/vnd.example.v1+json, where example is the name of your API and v1 is the version.

For example, my API is called dream and the latest version is 2.1, so the header would be: Accept: application/vnd.dream.v2.1+json.

Another simpler option, although less recommended, is to use a custom header such as Api-Version, where the value would be 2.1. That said, avoid the classic X- prefix (X-API-Version): it has been discouraged since 2012 by RFC 6648.

How do I handle pagination?

You will need to include metadata in the response to indicate the pagination with meta.

A pagination example could be:

{ "data": [...], "meta": { "total": 150, "page": 1, "perPage": 10, "hasNext": true, "hasPrevious": false }, "_links": { "self": {"href": "/api/items?page=1", "method": "GET"}, "next": {"href": "/api/items?page=2", "method": "GET"}, "first": {"href": "/api/items?page=1", "method": "GET"}, "last": {"href": "/api/items?page=15", "method": "GET"} } }
Where:

  • total: Total number of available items. Not on the current page, but in the whole collection.
  • page: Current page number. It can never be 0 or negative.
  • perPage: Number of items per page.
  • hasNext: Indicates whether there are more pages available.
  • hasPrevious: Indicates whether there are previous pages available.
  • _links: Links to navigate the pagination, such as- next,- previous,- first,- lastand- self. When a link does not apply (there is no previous page on the first page), it is omitted, not set to- null: that is how HAL does it and it avoids ambiguity for the client.

You can also use templates to let the client specify the page number:

{ "_links": { "page": { "href": "/api/items?page={page}&perPage={perPage}", "method": "GET", "templated": true } } }

How do I handle errors?

Here there is a standard, although few people know it: RFC 9457 (Problem Details for HTTP APIs, which updates RFC 7807 from 2016). It defines the application/problem+json media type with five fields: type (a URI identifying the problem category), title (a short, stable summary), status (the HTTP status code), detail (an explanation of this specific occurrence) and instance (the URI of the affected resource). And it allows you to add your own extension fields.

For example, a response with a 404 Not Found status and the Content-Type: application/problem+json header would carry this body:

{ "type": "https://example.com/errors/article-not-found", "title": "Article not found", "status": 404, "detail": "There is no article with id c9bb4e4a", "instance": "/api/blog/c9bb4e4a" }
If you are starting an API from scratch, my advice is to use Problem Details: it is the standard and your clients will thank you. In my projects I use my own convention (type, errors, data and meta) that integrates with the response structure of my use cases; if you are curious, I explain it in Implementing Clean Architecture in Python.

Conclusions

A true RESTful API, the one Fielding described, is not an evolution of REST: it is complete REST, the level 3 of the Richardson Maturity Model that almost nobody reaches. While popular usage stops at resources, verbs and status codes, hypermedia adds what turns an API into a self-descriptive and navigable interface.

For errors you already have a standard, RFC 9457: use it and your clients will always know what to expect.

In short, a well-implemented RESTful API does not just transfer data; it takes the client by the hand through all of its resources, creating an experience that is intuitive and resilient to change. Which is essential for a long-term project.

Sources

If you want to keep digging into the topic, here are some recommended sources:

  • Roy Fielding's Dissertation
  • REST APIs must be hypertext-driven by Roy Fielding.
  • Richardson Maturity Model by Martin Fowler.
  • RFC 9457: Problem Details for HTTP APIs
  • HAL - Hypertext Application Language
  • REST Architectural Constraints by Lokesh Gupta.

It would also be interesting to read more about HAL and JSON-LD, since they are formats that make it easier to implement HATEOAS and the self-description of RESTful APIs.

  • HATEOAS (Hypermedia as the Engine of Application State)
  • The 6 rules of a RESTful API
    1. Don't depend on a single protocol
    1. Don't change the protocol
    1. Focus on media types, not URIs
    1. Don't assume URI structures
    1. Avoid resource "types"
    1. Start with a bookmark (root resource) and let the client explore
  • FAQ
  • How do I indicate the version of the API or endpoint?
  • How do I handle pagination?
  • How do I handle errors?
  • Conclusions
  • Sources

This work is under a Attribution-NonCommercial-NoDerivatives 4.0 International license.

Will you buy me a coffee?

This is how I keep writing without ads or paywalls.