HATEOAS and Hypermedia-Driven APIs

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

TRY NANO BANANA FOR FREE

HATEOAS and Hypermedia-Driven APIs

TRY NANO BANANA FOR FREE
Contents

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:

Always include a self link:

{
  "id": "123",
  "name": "Max",
  "url": "/v1/pets/123"
}

This helps with debugging and logging.

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.