Meta Description: HATEOAS is the most controversial REST constraint. Learn what it is, why few APIs implement it, and when hypermedia links actually make sense.
Keywords: hateoas, hypermedia api, rest maturity model, rest constraints, api design patterns, hypermedia links
Word Count: ~2,000 words
You've built a REST API. It uses proper HTTP methods, status codes, and resource naming. But is it truly RESTful?
According to Roy Fielding (who invented REST), probably not. Most APIs claiming to be "REST" are missing a key constraint: HATEOAS.
HATEOAS (Hypermedia as the Engine of Application State) means your API responses include links to related resources and available actions. Clients navigate your API by following links, not by constructing URLs.
Few APIs implement HATEOAS. Is it worth the effort? Let's find out.
What Is HATEOAS?
HATEOAS means API responses include hypermedia links that tell clients what they can do next.
Without HATEOAS:
{
"id": "019b4132-70aa-764f-b315-e2803d882a24",
"name": "Max",
"species": "DOG",
"status": "AVAILABLE"
}
Clients must know: - How to construct URLs for related resources - What actions are available - What the next steps are
With HATEOAS:
{
"id": "019b4132-70aa-764f-b315-e2803d882a24",
"name": "Max",
"species": "DOG",
"status": "AVAILABLE",
"_links": {
"self": {
"href": "/v1/pets/019b4132-70aa-764f-b315-e2803d882a24"
},
"adopt": {
"href": "/v1/pets/019b4132-70aa-764f-b315-e2803d882a24/adoptions",
"method": "POST"
},
"vaccinations": {
"href": "/v1/pets/019b4132-70aa-764f-b315-e2803d882a24/vaccinations"
},
"owner": {
"href": "/v1/users/019b4137-6d8e-5c2b-e9f4-a3b5c8d7e2f1"
}
}
}
The response tells clients: - Where to find this resource (self) - How to adopt this pet (adopt) - Where to see vaccinations (vaccinations) - Who owns this pet (owner)
Clients follow links instead of constructing URLs.
The REST Maturity Model
Leonard Richardson created a maturity model for REST APIs:
Level 0: The Swamp of POX (Plain Old XML)- Single endpoint - Single HTTP method (usually POST) - RPC-style
POST /api
{ "method": "getPet", "id": "123" }
Level 1: Resources- Multiple endpoints - Each resource has its own URL
GET /pets/123
GET /orders/456
Level 2: HTTP Verbs- Proper use of HTTP methods - Correct status codes
GET /pets/123
POST /pets
DELETE /pets/123
Level 3: Hypermedia Controls (HATEOAS)- Responses include links - Clients navigate by following links
{
"id": "123",
"_links": {
"self": { "href": "/pets/123" },
"adopt": { "href": "/pets/123/adoptions" }
}
}
Most APIs are Level 2. Few reach Level 3.
Why HATEOAS Is Controversial
Argument For: Decoupling
HATEOAS decouples clients from URL structure. You can change URLs without breaking clients.
Without HATEOAS, clients hardcode URLs:
const adoptUrl = `/pets/${petId}/adoptions`;
If you change the URL structure, clients break.
With HATEOAS, clients follow links:
const adoptUrl = pet._links.adopt.href;
You can change URLs without breaking clients (as long as link relations stay the same).
Argument For: Discoverability
HATEOAS makes APIs self-documenting. Clients discover available actions from responses.
{
"id": "123",
"status": "AVAILABLE",
"_links": {
"adopt": { "href": "/pets/123/adoptions" }
}
}
The presence of the adopt link tells clients adoption is available.
When the pet is adopted:
{
"id": "123",
"status": "ADOPTED",
"_links": {
"owner": { "href": "/users/456" }
}
}
The adopt link disappears. The owner link appears. Clients adapt automatically.
Argument Against: Complexity
HATEOAS adds complexity: - More data in responses (bandwidth cost) - Clients must parse links - Link relations must be documented - Harder to implement
For simple APIs, this overhead isn't worth it.
Argument Against: Limited Client Support
Most HTTP clients don't have built-in HATEOAS support. You need custom code to follow links.
// Manual link following
async function adoptPet(pet) {
const adoptLink = pet._links.adopt;
if (!adoptLink) {
throw new Error('Adoption not available');
}
const response = await fetch(adoptLink.href, {
method: adoptLink.method || 'GET'
});
return response.json();
}
Without HATEOAS, it's simpler:
async function adoptPet(petId) {
const response = await fetch(`/pets/${petId}/adoptions`, {
method: 'POST'
});
return response.json();
}
Argument Against: Caching Issues
HATEOAS responses include URLs that might change. This complicates caching.
If you change a URL, cached responses contain old links. Clients might follow broken links.
When HATEOAS Makes Sense
HATEOAS isn't always worth it, but it shines in specific scenarios.
Use Case 1: Complex Workflows
When your API has multi-step workflows with conditional paths, HATEOAS helps.
Example: Pet adoption workflow
Step 1: View available pets
{
"id": "123",
"status": "AVAILABLE",
"_links": {
"apply": { "href": "/pets/123/applications", "method": "POST" }
}
}
Step 2: Submit application
{
"id": "app-456",
"status": "PENDING",
"_links": {
"self": { "href": "/applications/app-456" },
"cancel": { "href": "/applications/app-456", "method": "DELETE" }
}
}
Step 3: Application approved
{
"id": "app-456",
"status": "APPROVED",
"_links": {
"complete": { "href": "/applications/app-456/complete", "method": "POST" },
"pet": { "href": "/pets/123" }
}
}
The links guide clients through the workflow. Clients don't need to know the entire flow upfront.
Use Case 2: Evolving APIs
If your API structure changes frequently, HATEOAS provides flexibility.
You can: - Move resources to different URLs - Add new actions - Remove deprecated actions
Clients adapt automatically by following links.
Use Case 3: Public APIs with Many Clients
When you have thousands of clients you don't control, HATEOAS reduces breaking changes.
Clients that follow links continue working when you change URLs. Clients that hardcode URLs break.
Use Case 4: API Exploration Tools
HATEOAS enables better API exploration. Tools can automatically discover available actions and build UI dynamically.
HATEOAS Formats
Several formats exist for hypermedia links.
HAL (Hypertext Application Language)
The most popular format:
{
"id": "123",
"name": "Max",
"_links": {
"self": { "href": "/pets/123" },
"adopt": { "href": "/pets/123/adoptions" }
},
"_embedded": {
"owner": {
"id": "456",
"name": "John",
"_links": {
"self": { "href": "/users/456" }
}
}
}
}
Pros: Simple, widely supported Cons: Limited metadata
JSON:API
A more opinionated format:
{
"data": {
"type": "pets",
"id": "123",
"attributes": {
"name": "Max"
},
"relationships": {
"owner": {
"links": {
"related": "/users/456"
}
}
},
"links": {
"self": "/pets/123"
}
}
}
Pros: Standardized structure, includes relationships Cons: Verbose, steep learning curve
Siren
Action-oriented format:
{
"class": ["pet"],
"properties": {
"id": "123",
"name": "Max"
},
"actions": [
{
"name": "adopt",
"method": "POST",
"href": "/pets/123/adoptions",
"fields": [
{ "name": "userId", "type": "text" }
]
}
],
"links": [
{ "rel": ["self"], "href": "/pets/123" }
]
}
Pros: Describes actions with parameters Cons: Complex, less adoption
Practical Recommendation
For most APIs, don't implement full HATEOAS. The complexity outweighs the benefits.
Instead, use a hybrid approach:
1. Include Self Links
Always include a self link:
{
"id": "123",
"name": "Max",
"url": "/v1/pets/123"
}
This helps with debugging and logging.
2. Include Related Resource URLs
Include URLs for related resources:
{
"id": "123",
"name": "Max",
"ownerUrl": "/v1/users/456",
"vaccinationsUrl": "/v1/pets/123/vaccinations"
}
Clients can follow these without constructing URLs.
3. Document Available Actions
In documentation, show what actions are available for each resource state:
Pet (status: AVAILABLE)
- GET /pets/{id} - View pet
- POST /pets/{id}/adoptions - Start adoption
- GET /pets/{id}/vaccinations - View vaccinations
Pet (status: ADOPTED)
- GET /pets/{id} - View pet
- GET /pets/{id}/owner - View owner
This gives clients the benefits of discoverability without the complexity of full HATEOAS.
Conclusion
HATEOAS is theoretically elegant but practically complex. Most APIs don't need it.
Skip HATEOAS if: - Your API is simple - You control all clients - URL structure is stable - You want to minimize response size
Consider HATEOAS if: - You have complex workflows - Your API structure changes frequently - You have many third-party clients - You want maximum decoupling
For most teams, a hybrid approach (self links + related resource URLs) provides the best balance of simplicity and flexibility.
Related Articles: - The 7 RESTful Design Principles Every API Must Follow - Stop Using Action Verbs in Your API URLs - API Versioning Done Right: URL vs Header vs Content Negotiation
Try It Yourself: The Modern PetStore API includes optional HATEOAS links. Enable them with the ?include_links=true parameter to see hypermedia in action.