Skip to main content

Resources, Representations, and HATEOAS

Design REST resources with rich representations and hypermedia linking

TL;DR

In REST, resources are the core abstraction—identifiable entities (users, posts, orders) accessible via URIs. A resource may have multiple representations (JSON, XML, HTML) negotiated via Accept headers. HATEOAS (Hypermedia As The Engine Of Application State) uses links to guide clients through workflows, reducing coupling and enabling server-side evolution. While HATEOAS is optional, understanding it prevents architectural mistakes and improves long-term maintainability.

Learning Objectives

  • Distinguish between resources, representations, and their URIs
  • Apply content negotiation for multiple formats
  • Understand HATEOAS and its practical benefits
  • Design self-documenting APIs using hypermedia links
  • Recognize the REST maturity model and where you stand

Motivating Scenario

Your e-commerce API returns a user: { "id": 123, "name": "Alice", "email": "alice@example.com" }. Later, clients ask for addresses. You add an addresses array to the response. Then they need order history. You add that too. Soon, the response contains nested structures clients don't need, bloating bandwidth and confusing documentation.

With hypermedia, you'd return: { "id": 123, "name": "Alice", "_links": { "self": { "href": "/users/123" }, "addresses": { "href": "/users/123/addresses" }, "orders": { "href": "/users/123/orders" } } }. Clients follow links on demand. The server can change URLs without breaking clients. Evolution becomes straightforward.

Core Concepts

Resources vs Representations

A resource is an abstract concept—the user Alice. A representation is a specific format of that resource. The same resource /users/123 might be represented as JSON, XML, or HAL. The resource is singular; representations are multiple.

Content Negotiation

Clients use the Accept header to request preferred formats. The server responds with Content-Type. This decouples the resource model from format concerns and lets you add new formats without API changes.

HATEOAS: Hypermedia as the Engine

HATEOAS means responses include links that tell clients what actions are possible next. Instead of clients knowing URLs hardcoded, links guide navigation. This enables:

  • Loose coupling: Clients don't hardcode URL patterns
  • Server-side evolution: URLs can change without client updates
  • Self-discovery: APIs become self-documenting

Practical Example

GET /users/123
{
"id": 123,
"name": "Alice",
"email": "alice@example.com",
"orders": [
{ "id": 1001, "total": 99.99 },
{ "id": 1002, "total": 149.50 }
],
"addresses": [
{ "type": "billing", "street": "123 Main St" },
{ "type": "shipping", "street": "456 Oak Ave" }
}

Issues: Clients receive unwanted nested data. Adding new relations requires API changes. URLs hardcoded in clients.

REST Maturity Model

The Richardson Maturity Model ranks REST implementations:

LevelCharacteristicsExample
0 (POX)HTTP tunneling, RPC-stylePOST /api?action=getUser&id=123
1Resources, but not HTTP methodsGET /users/123, POST /updateUser (wrong verb)
2HTTP verbs and status codesGET /users/123 returns 200, PUT /users/123 returns 204
3HATEOAS with hypermedia linksResponses include navigable links; self-documenting

Most public APIs today are Level 2. Level 3 (HATEOAS) is valuable for complex workflows and long-term evolution but adds complexity. Choose based on your API's scope and client diversity.

When to Use HATEOAS

Use HATEOAS When
  1. API has many resources with complex relationships
  2. You want to evolve URLs without client updates
  3. Clients need guidance on available actions
  4. Long-term stability is a priority
  5. API documentation should be self-contained
Skip HATEOAS When
  1. Client tightly coupled to your organization
  2. Simple, stable resource model
  3. Performance critical (adds link overhead)
  4. Clients prefer static documentation
  5. Building internal API for single application

Patterns and Pitfalls

Pitfall: Returning all nested relations in every response. Users don't always need order history. Use links and let clients request what they need via separate requests or ?include=orders.

Pitfall: Inventing custom link formats. Use standard formats: HAL+JSON, JSON:API, or Siren. Clients understand these conventions.

Pitfall: Breaking HATEOAS by hardcoding URLs in client code. If clients still hardcode /users/{id}, hypermedia adds no value.

Pattern: Use links for navigation and state transitions, not for every possible piece of data. A link to "addresses" is valuable; a link to "default address" is probably data that belongs in the representation.

Design Review Checklist

  • Identified all resources in your API (nouns, not verbs)
  • Each resource has a stable, meaningful URI
  • Content negotiation handled (Accept header respected)
  • Documented all representations your API supports
  • Decided: HATEOAS needed or Level 2 sufficient?
  • If using HATEOAS, chose standard format (HAL, JSON:API, Siren)
  • Links clearly document available actions
  • Error responses don't include contradictory links

Representation Format Examples

JSON Representation

GET /users/123
Accept: application/json

{
"id": 123,
"name": "Alice",
"email": "alice@example.com",
"created_at": "2025-09-10T10:00:00Z"
}

HAL+JSON (with HATEOAS)

GET /users/123
Accept: application/hal+json

{
"id": 123,
"name": "Alice",
"email": "alice@example.com",
"created_at": "2025-09-10T10:00:00Z",
"_links": {
"self": { "href": "/users/123" },
"all_users": { "href": "/users" },
"addresses": { "href": "/users/123/addresses" },
"orders": { "href": "/users/123/orders" },
"edit": { "href": "/users/123", "method": "PUT" },
"delete": { "href": "/users/123", "method": "DELETE" }
}
}

JSON:API Representation

GET /users/123

{
"data": {
"type": "users",
"id": "123",
"attributes": {
"name": "Alice",
"email": "alice@example.com",
"created_at": "2025-09-10T10:00:00Z"
},
"relationships": {
"addresses": {
"data": [
{ "type": "addresses", "id": "1" }
]
},
"orders": {
"data": [
{ "type": "orders", "id": "5001" }
]
}
}
}
}

Content Negotiation in Practice

Server-Driven Negotiation

Client specifies format via Accept header:

GET /api/users/123
Accept: application/json

or

GET /api/users/123
Accept: application/xml

or

GET /api/users/123
Accept: text/csv

Agent-Driven Negotiation

Server provides list of available formats:

GET /api/users/123

HTTP/1.1 300 Multiple Choices
Link: </api/users/123>; rel="self"; type="application/json"
Link: </api/users/123.xml>; rel="alternate"; type="application/xml"
Link: </api/users/123.csv>; rel="alternate"; type="text/csv"

Self-Check

  • Can you describe the difference between a resource and a representation?
  • How would content negotiation help your API scale to multiple clients?
  • What is one business scenario where HATEOAS would simplify client code?
  • Can you design HATEOAS links for your critical resources?
  • How would you handle content negotiation errors?
One Takeaway

Resources are nouns, representations are formats, and hypermedia links are the connective tissue that lets your API evolve without breaking clients. Choose your representation format strategically—HAL and JSON:API are mature standards with library support.

HATEOAS Maturity Levels

Level 0: Plain JSON (No HATEOAS)

GET /users/123
{
"id": 123,
"name": "Alice"
}
// Client must hardcode URL patterns
client_code: "/users/{id}" hardcoded
GET /users/123
{
"id": 123,
"name": "Alice",
"_links": {
"self": { "href": "/users/123" }
}
}
// Client still hardcodes related resource paths
GET /users/123
{
"id": 123,
"name": "Alice",
"_links": {
"self": { "href": "/users/123" },
"all_users": { "href": "/users" },
"orders": { "href": "/users/123/orders" },
"addresses": { "href": "/users/123/addresses" }
}
}
// Client can discover related resources
GET /users/123
{
"id": 123,
"name": "Alice",
"status": "active",
"_links": {
"self": { "href": "/users/123" },
"all_users": { "href": "/users" },
"orders": { "href": "/users/123/orders" },
"addresses": { "href": "/users/123/addresses" },
"edit": { "href": "/users/123", "method": "PUT" },
"deactivate": { "href": "/users/123/deactivate", "method": "POST" },
"delete": { "href": "/users/123", "method": "DELETE" }
}
}
// Server tells client what actions are possible
// If user inactive, "deactivate" link would be absent
// Client doesn't hardcode which actions available

Choosing Your Representation Format

JSON (Plain)

Pros: Simple, lightweight, universal Cons: No discovery, no hypermedia Use for: Internal APIs, tightly coupled clients

HAL (Hypertext Application Language)

Pros: Simple HATEOAS, well-defined standard Cons: Limited action description Libraries: Spring HATEOAS (Java), Halogen (Scala), Roar (Ruby) Use for: RESTful APIs with hypermedia

JSON:API

Pros: Complete spec, handles relationships, filtering, pagination Cons: More complex, opinionated Libraries: All major languages supported Use for: Complex APIs with many relationships

Siren

Pros: Rich action description, fields with validation Cons: Less widely adopted Libraries: Limited ecosystem Use for: APIs with complex state transitions

Content Negotiation in Production

# Flask example: respond to multiple formats
@app.route('/users/<int:user_id>', methods=['GET'])
def get_user(user_id):
user = User.query.get(user_id)

# Check Accept header
accept_header = request.headers.get('Accept', 'application/json')

if 'application/xml' in accept_header:
return to_xml(user), 200, {'Content-Type': 'application/xml'}
elif 'application/csv' in accept_header:
return to_csv(user), 200, {'Content-Type': 'text/csv'}
elif 'application/hal+json' in accept_header:
return to_hal_json(user), 200, {'Content-Type': 'application/hal+json'}
else:
# Default to JSON
return to_json(user), 200, {'Content-Type': 'application/json'}

# Client specifies preference (can accept multiple)
GET /users/123
Accept: application/hal+json, application/json;q=0.9, */*;q=0.1
// Server responds with HAL+JSON if available, falls back to JSON

Pagination with HATEOAS

Pagination is challenging with HATEOAS—users need to traverse pages:

GET /users?page=2&limit=10
{
"data": [
{ "id": 11, "name": "User 11" },
{ "id": 12, "name": "User 12" },
...
],
"_links": {
"self": { "href": "/users?page=2&limit=10" },
"first": { "href": "/users?page=1&limit=10" },
"previous": { "href": "/users?page=1&limit=10" },
"next": { "href": "/users?page=3&limit=10" },
"last": { "href": "/users?page=10&limit=10" }
},
"total": 100,
"pages": 10
}

Benefits of HATEOAS pagination:

  • Client doesn't need to construct URLs
  • Server can change pagination strategy (limit, offset → cursor-based)
  • Discoverability: client knows available pages

Conditional Requests with HATEOAS

HATEOAS can include conditional request information:

GET /users/123
{
"id": 123,
"name": "Alice",
"email": "alice@example.com",
"_links": {
"self": {
"href": "/users/123",
"title": "Get user",
"type": "application/hal+json"
},
"edit": {
"href": "/users/123",
"method": "PUT",
"title": "Update user",
"schema": { ... }
}
},
"_embedded": {
"orders": [
{
"id": 1001,
"amount": 99.99,
"_links": { "self": { "href": "/orders/1001" } }
}
]
}
}

HATEOAS in Error Responses

Error responses should also include helpful links:

HTTP/1.1 404 Not Found
Content-Type: application/hal+json

{
"error": "Not Found",
"message": "User 999 does not exist",
"status": 404,
"_links": {
"help": { "href": "/docs/errors#404" },
"search": { "href": "/users?search=..." },
"create": { "href": "/users", "method": "POST" }
}
}

Hypermedia-Driven API Example

Complete example of hypermedia-driven workflow:

1. Client starts at root
GET /
{
"_links": {
"users": { "href": "/users" },
"products": { "href": "/products" },
"search": { "href": "/search{?q}" } // URI Template
}
}

2. Client discovers users collection
GET /users
{
"data": [ { "id": 1, "name": "Alice" } ],
"_links": {
"self": { "href": "/users" },
"create": { "href": "/users", "method": "POST" }
}
}

3. Client follows user link
GET /users/1
{
"id": 1,
"name": "Alice",
"_links": {
"self": { "href": "/users/1" },
"orders": { "href": "/users/1/orders" },
"edit": { "href": "/users/1", "method": "PUT" },
"delete": { "href": "/users/1", "method": "DELETE" }
}
}

4. Client discovers orders
GET /users/1/orders
{
"data": [ { "id": 1001, "total": 99.99 } ],
"_links": {
"self": { "href": "/users/1/orders" },
"create": { "href": "/users/1/orders", "method": "POST" }
}
}

5. Client discovers specific order
GET /orders/1001
{
"id": 1001,
"total": 99.99,
"_links": {
"self": { "href": "/orders/1001" },
"user": { "href": "/users/1" },
"items": { "href": "/orders/1001/items" },
"cancel": { "href": "/orders/1001/cancel", "method": "POST" }
}
}

No hardcoded URLs; entire API discovered through links.

Next Steps

  • Read URI Design & Methods to learn how to structure URIs clearly
  • Study Error Formats & Problem Details for consistent error representations
  • Explore frameworks like HAL, JSON:API, or Siren for HATEOAS implementation
  • Implement content negotiation to support multiple clients
  • Design your first hypermedia-driven API workflow

References

  • Richardson Maturity Model (Leonard Richardson)
  • Fielding Dissertation on REST (Roy T. Fielding)
  • HAL Specification
  • JSON:API Specification
  • RESTful Web Services (Leonard Richardson & Sam Ruby)