While designing RESTful APIs, developers go through several stages of REST maturity. One of the most significant concepts to reach higher levels of maturity is HATEOAS, aka Hypermedia as the Engine of Application State. This helps clients navigate an API in a dynamic manner by following hypermedia links contained within responses—reducing tight coupling and enhancing discoverability.

What is HATEOAS?

HATEOAS is a constraint in REST that dictates that a client only communicate with a RESTful service using hypermedia provided dynamically by the server. That is, each API response includes suitable links to the next available action.

Non-HATEOAS Response:

{
  "id": 1,
  "title": "Harry Potter",
  "author": "J. K. Rowling"
}

HATEOAS Response:

{
  "id": 1,
  "title": "Harry Potter",
  "author": "J. K. Rowling",
  "_links": {
    "self": { "href": "/api/books/1" },
    "purchase": { "href": "/api/books/1", "method": "POST" }
  }
}

This approach makes the client less dependent on hardcoded routes and gives the server more control over the interaction flow.

Implementing HATEOAS in C#

Let’s go step-by-step through building a simple HATEOAS-enabled API.

Step 1: Define a Resource Model

public class BookResource
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Author { get; set; }
    public decimal Price { get; set; }

    public List<LinkResource> Links { get; set; } = new();
}

public class LinkResource
{
    public string Href { get; set; }
    public string Rel { get; set; }
    public string Method { get; set; }
}
public class LinkService
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public LinkService(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public string GenerateLink(string routeName, object routeValues)
    {
        var request = _httpContextAccessor.HttpContext.Request;
        return new Uri(new Uri($"{request.Scheme}://{request.Host}"), 
                       request.HttpContext.GetRouteUrl(routeName, routeValues)).ToString();
    }
}
[ApiController]
[Route("api/books")]
public class BooksController : ControllerBase
{
    private readonly LinkService _linkService;

    public BooksController(LinkService linkService)
    {
        _linkService = linkService;
    }

    [HttpGet("{id}", Name = "GetBook")]
    public IActionResult GetBook(int id)
    {
        // sample data
        var book = new BookResource
        {
            Id = id,
            Title = "Test title",
            Author = "Test author",
            Price = 20
        };

        // Adding hypermedia links
        book.Links.Add(new LinkResource
        {
            Href = _linkService.GenerateLink("GetBook", new { id }),
            Rel = "self",
            Method = "GET"
        });

        book.Links.Add(new LinkResource
        {
            Href = _linkService.GenerateLink("PurchaseBook", new { id }),
            Rel = "purchase",
            Method = "POST"
        });

        return Ok(book);
    }

    [HttpPost("{id}/purchase", Name = "PurchaseBook")]
    public IActionResult PurchaseBook(int id)
    {
        // purchase logic here
        return Ok(new { Message = $"Book with ID {id} purchased successfully!" });
    }
}

Step 4: Register dependency in Startup.cs

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
        services.AddHttpContextAccessor();
        // register the Link Service
        services.AddScoped<LinkService>();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
}

Handling Nested Resources and Relationships

In real-world applications, resources often have relationships with other resources. Implementing HATEOAS in these cases means including hypermedia links that reflect those associations, allowing clients to seamlessly traverse related data.

Example: Book and Author Relationship

Suppose you have separate endpoints for Books and Authors. A HATEOAS-compliant response for a book should include a link to its author.

{
  "id": 1,
  "title": "Harry Potter",
  "author": "J. K. Rowling",
  "_links": {
    "self": { "href": "/api/books/1" },
    "purchase": { "href": "/api/books/1", "method": "POST" },
    "author": { "href": "/api/authors/42" }
  }
}

Updating the Resource Model

You can enrich the BookResource with related resource links:

book.Links.Add(new LinkResource
{
    Href = _linkService.GenerateLink("GetAuthor", new { id = 42 }),
    Rel = "author",
    Method = "GET"
});

This allows clients to discover additional data dynamically, without hardcoding relationships or endpoint paths.

Nested Collections

When returning a list of resources (e.g., all books by an author), you can add links to each item as well as to the collection itself:

{
  "_links": {
    "self": { "href": "/api/authors/42/books" },
    "author": { "href": "/api/authors/42" }
  },
  "items": [
    {
      "id": 1,
      "title": "Harry Potter",
      "_links": {
        "self": { "href": "/api/books/1" },
        "purchase": { "href": "/api/books/1", "method": "POST" }
      }
    },
    {
      "id": 2,
      "title": "Fantastic Beasts",
      "_links": {
        "self": { "href": "/api/books/2" },
        "purchase": { "href": "/api/books/2", "method": "POST" }
      }
    }
  ]
}

Benefits of HATEOAS

  • Discoverability: Clients can explore and interact with the API without prior knowledge of endpoints.
  • Decoupling: Clients rely on links provided by the server, not on fixed URL structures.
  • API Evolution: Easier to modify endpoints or flows without breaking clients.

When Not to Use HATEOAS

While HATEOAS is powerful, it may not always be necessary:

  • If your API is small and internal.
  • If clients are tightly controlled and hardcoding is acceptable.
  • If performance overhead of generating links is not justified.

Conclusion

HATEOAS enhances your REST APIs with dynamic navigation to make them stronger and more adaptable, and this makes them better. HATEOAS is simple to do in ASP.NET Core, and it can contribute significantly to client experience—especially in public or big APIs.


<
Previous Post
Innovations in Caching: Microsoft HybridCache
>
Next Post
Real-Time Updates with Server-Sent Events (SSE) in .NET