Skip to content

Dynamic Discovery in HATEOAS — How Clients Navigate APIs at Runtime

DodaTech Updated 2026-06-28 2 min read

In this tutorial, you will learn about Dynamic Discovery in HATEOAS. We cover key concepts, practical examples, and best practices to help you master this topic.

Dynamic discovery is the core benefit of HATEOAS, where clients start at a root entry point and discover available resources and actions by following links, without any hardcoded URLs.

What You'll Learn

  • How clients discover API capabilities at runtime
  • The role of the root entry point in discovery
  • Implementing generic hypermedia clients

Why It Matters

Dynamic discovery enables generic API clients that work with any HATEOAS-compliant API. This is the vision of truly RESTful systems — clients and servers evolving independently.

flowchart LR
    A["GET /api"] --> B["Entry Point\nResponse with links"]
    B --> C["GET /users"]
    B --> D["GET /orders"]
    B --> E["GET /products"]
    C --> F["User's links to\norders, profile, edit"]
    D --> G["Order's links to\nitems, payments, cancel"]
    style B fill:#dbeafe,stroke:#2563eb

Code Examples

# Generic HATEOAS client
class HypermediaClient:
    def __init__(self, entry_url):
        self.session = requests.Session()
        self.entry = self.session.get(entry_url).json()

    def follow(self, rel, method="GET", data=None):
        link = self.entry.get("_links", {}).get(rel)
        if not link:
            raise KeyError(f"No link with rel '{rel}'")
        url = f"{self.session.headers.get('base', '')}{link['href']}"
        return self.session.request(method, url, json=data)

client = HypermediaClient("https://api.example.com/api")
users = client.follow("users").json()
order_link = users[0]["_links"]["orders"]
# Client discovers next steps dynamically
// JavaScript hypermedia client
class HypermediaClient {
  constructor(baseUrl) {
    this.baseUrl = baseUrl;
  }

  async discover() {
    this.root = await this.fetchRel('self');
  }

  async fetchRel(rel) {
    if (!this.current?._links?.[rel]) {
      throw new Error(`No ${rel} link`);
    }
    const link = this.current._links[rel];
    const res = await fetch(`${this.baseUrl}${link.href}`);
    return res.json();
  }
}

Common Mistakes

1. Having No Entry Point

Every HATEOAS API should have a root resource that links to all major collections.

2. Hardcoding URLs in Generic Clients

A truly generic client never constructs URLs. It always follows links.

The root response should link to itself for discovery bootstrapping.

Links can change. Clients should re-fetch resources to get current links.

Generic clients should gracefully skip unknown rels.

Practice Questions

  1. What is the role of the entry point in HATEOAS?
  2. How does a client discover available actions without documentation?
  3. What happens if a client follows an outdated link?
  4. Why should clients avoid caching links permanently?
  5. Can a generic HATEOAS client work with any API?

Answers:

  1. The entry point is the root URL that links to all primary resources.
  2. By reading _links and _actions from API responses.
  3. It may get a 404 or redirected to the current URL.
  4. Resource states change, and available links change with them.
  5. Yes, as long as the API consistently follows HATEOAS conventions.

Challenge: Build a minimal generic hypermedia client that can navigate any HATEOAS API. Start with an entry point and discover resources through links only.

FAQ

Does dynamic discovery work with non-HATEOAS APIs?

: No. The API must include hypermedia links in all responses.

What happens if a link returns 404?

: The client should handle gracefully and possibly re-discover from the entry point.

Can dynamic discovery work across API versions?

: Yes. The entry point can link to version-specific resources.

What's Next

Build a HATEOAS Client in your language of choice, then compare HATEOAS vs GraphQL.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro