Implementing HATEOAS in your ASP NET Core web API: Enhancing API Discoverability and Navigability

Implementing HATEOAS in your ASP NET Core web API: Enhancing API Discoverability and Navigability

As developers, we strive to build robust and user-friendly web APIs that provide a seamless experience for clients. One powerful approach to achieve this is by implementing HATEOAS (Hypermedia as the Engine of Application State) in our ASP.NET Core Web APIs as this provides a handful of benefits for the client that you will see in just a bit. In the last post, we talked about exposing related entities with Entity Framework Core in our web API and how to avoid some known problems with nested entities so I will leave you the link for that post below. Now let’s get into today’s topic, implementing HATEOAS.

Exposing Related Entities in your Web API
When working on building a powerful API you will find yourself dealing with resources and more resources and these “resources” have “relations” between them and this is powerful as we can leverage the power of relational data to create amazing ways of displaying information or use it. But if you

Overview of HATEOAS and its Significance in web APIs

HATEOAS is an architectural principle that emphasizes the use of hypermedia links to drive the interaction between clients and APIs. It enables us to create self-describing APIs where clients can dynamically discover available resources and navigate through them using standardized links. By providing hypermedia-driven responses, we enhance the discoverability and navigability of our APIs, making them more intuitive and easier to consume.

Benefits of using HATEOAS in ASP NET Core Web APIs

Implementing HATEOAS in our ASP NET Core Web APIs brings several benefits that greatly improve the user experience and API design:

  1. Improved Discoverability: HATEOAS enables the automatic discovery of resources and endpoints within an API. By including hypermedia links in API responses, clients can dynamically navigate through the available resources without the need for prior knowledge of the API structure. This improves the discoverability of the API and reduces the learning curve for developers integrating with it.
  2. Enhanced Navigability: With HATEOAS, clients can seamlessly navigate through related resources by following hypermedia links. This eliminates the need for hard-coded dependencies on specific endpoints and allows for flexible exploration of the API. Clients can traverse from one resource to another, making the API more dynamic and adaptable to changes over time.
  3. Flexible API Evolution: HATEOAS promotes a decoupled architecture between clients and servers. By using hypermedia links to represent relationships between resources, the API design becomes more flexible and allows for gradual changes without breaking existing client applications. It enables versioning and evolution of the API without requiring clients to update their code every time the API changes.
  4. Simplified Integration: HATEOAS reduces the complexity of integrating with an API by providing a standardized way to interact with resources. Clients can rely on the hypermedia links to understand the available actions and the next steps to take. This simplifies the integration process and promotes interoperability between different clients and servers.
  5. Improved User Experience: By leveraging HATEOAS, we can create more intuitive and user-friendly APIs. Clients can follow a consistent navigation pattern based on the hypermedia links provided in the responses. This reduces the cognitive load on developers and makes it easier to understand and interact with the API, resulting in a better user experience.

Definition and core principles of HATEOAS

HATEOAS revolves around the concept of including hypermedia links in API responses. These links provide additional information and context to clients, allowing them to navigate through the API and discover related resources. By adhering to the core principles of HATEOAS, we create APIs that are self-describing, decoupled, and highly adaptable.

How HATEOAS enhances the discoverability and navigability of APIs

One of the key advantages of HATEOAS is its ability to enhance the discoverability of APIs. By including hypermedia links in our responses, we guide clients to explore available resources, eliminating the need for prior knowledge of the API structure. This makes our APIs more intuitive and reduces the learning curve for developers who integrate with our API.

Moreover, HATEOAS promotes navigability within the API. Clients can effortlessly traverse through related resources by following the hypermedia links, creating a dynamic and flexible experience. This eliminates the hard-coded dependencies on specific endpoints and allows for seamless API evolution and versioning.

Role of hypermedia in HATEOAS

Hypermedia plays a central role in HATEOAS by providing the means to represent and navigate resources. Hypermedia formats such as HAL (Hypertext Application Language) and JSON-LD (JSON Linked Data) provide standardized ways to include links and other metadata in API responses. By leveraging these hypermedia formats, we can ensure consistency and interoperability across different clients and APIs.

Hypermedia links can be used to indicate available actions, suggest related resources, or provide contextual information. They enable clients to follow a uniform navigation pattern, reducing the coupling between client and server and promoting a more flexible and adaptive API design.

Get the code from GitHub

Before starting to code I recommend if you want to follow along then go and grab the code from the GitHub repository and make sure you get the code from the “RelatedEntities” branch.

GitHub - Osempu/BlogAPI at RelatedEntities
Contribute to Osempu/BlogAPI development by creating an account on GitHub.

Install the RiskFirst.Hateoas nuget package

To implement HATEOAS in your project first you will need to install the RiskFirst nuget package using the following dotnet cli command.

dotnet add package RiskFirst.Hateoas

Add a Hateoas Response Model

Now add a new kind of response model for your main entity in this case the Post model that will be called PostHateoasResponse.

public class PostHateoasResponse : ILinkContainer
{
    public PostResponseDTO Data;
    private Dictionary<string, Link> links;

    [JsonPropertyName("links")]
    public Dictionary<string, Link> Links 
    { 
        get => links ?? (links = new Dictionary<string, Link>());
        set => links = value;
    }

    public void AddLink(string id, Link link)
    {
        Links.Add(id, link);
    }
}

This class represent the HATEOAS response for the post model and is implementing the ILinkContainer interface exposed by the RiskFirst.Hateoas package. The response model contains a Dictionary<string, Link> that contains the URLs to the methods (GET, POST, DELETE, PUT, PATCH). The AddLink method will just add new links to the links dictionary.

Implementing HATEOAS in a new Controller

To implement HATEOAS you can do it in your main controller but in this case we will create a new PostsController where you will be able to get the HATEOAS responses.

[ApiController]
[Route("api/hateoas/posts")]
public class PostsHateoasController : ControllerBase
{
    private readonly ILinksService linksService;
    private readonly IPostRepository postRepository;
    private readonly IMapper mapper;

    public PostsHateoasController(ILinksService linksService, IPostRepository postRepository, IMapper mapper)
    {
        this.linksService = linksService;
        this.postRepository = postRepository;
        this.mapper = mapper;
    }

    [HttpGet(Name = nameof(Get))]
    public async Task<IActionResult> Get([FromQuery] QueryParameters parameters)
    {
        var posts = await postRepository.GetPostAsync(parameters);
        var postsDto = mapper.Map<IEnumerable<PostResponseDTO>>(posts);

        var hateoasResults = new List<PostHateoasResponse>();

        foreach (var post in postsDto)
        {
            var hateoasResult = new PostHateoasResponse { Data = post};
            await linksService.AddLinksAsync(hateoasResult);
            hateoasResults.Add(hateoasResult);
        }

        return Ok(hateoasResults);
    }

    [HttpGet("{id:int}", Name = nameof(GetById))]
    public async Task<IActionResult> GetById(int id)
    {
        var post = await postRepository.GetPostAsync(id);
        var postDto = mapper.Map<PostResponseDTO>(post);

        var hateoasResult = new PostHateoasResponse{Data = postDto};
        await linksService.AddLinksAsync(hateoasResult);

        return Ok(hateoasResult);
    }

    [HttpPost(Name = nameof(Post))]
    public async Task<IActionResult> Post(AddPostDTO addPostDTO)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest();
        }

        var newPost = mapper.Map<AddPostDTO, Post>(addPostDTO);
        newPost.CreatedDate = DateTime.Now;
        await postRepository.AddAsync(newPost);
        return CreatedAtAction(nameof(Get), new { id = newPost.Id }, null);
    }

    [HttpPut("{id:int}", Name = nameof(Edit))]
    public async Task<IActionResult> Edit(int id, [FromBody] EditPostDTO editPostDto)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest();
        }

        var post = mapper.Map<EditPostDTO, Post>(editPostDto);

        post.LastUpdated = DateTime.Now;
        await postRepository.EditAsync(post);
	      return NoContent();
    }

    [HttpDelete("{id:int}", Name = nameof(Delete))]
    public async Task<IActionResult> Delete(int id)
    {
        await postRepository.DeleteAsync(id);
        return NoContent();
    }

    [HttpPatch("{id:int}", Name = nameof(Patch))]
    public async Task<ActionResult> Patch(int id, [FromBody] JsonPatchDocument<Post> doc)
    {
        var post = await postRepository.GetPostAsync(id);

        doc.ApplyTo(post);
        await postRepository.EditAsync(post);

        return NoContent();
    }
}

First, define the route attribute as [Route("api/hateoas/posts")] then in the constructor add a dependency to the ILinksService interface to create the links. Above every controller in the method attribute, you will have to add the name of the method to be able to call it from the services contained in the program class later. All the methods remain the same but for the Get methods for example in the Get method a list of PostHateoasResponse is created to populate it with a foreach loop that takes every PostResponseDto and creates the links for every method and finally adds that response to the List<PostHateoasResponse> and return that list. For the GetById method, it’s the same process but no foreach loop is needed as you will only create the links for a single resource and then return it.

Finally to set up the HATEOAS implementation you will have to register the LinksService in the services container as shown below.

builder.Services.AddLinks(config =>
{
    config.AddPolicy<PostHateoasResponse>(policy =>
    {
        policy
            .RequireRoutedLink(nameof(PostsHateoasController.Get), nameof(PostsHateoasController.Get))
            .RequireRoutedLink(nameof(PostsHateoasController.GetById), nameof(PostsHateoasController.GetById), _ => new {id = _.Data.Id})
            .RequireRoutedLink(nameof(PostsHateoasController.Edit), nameof(PostsHateoasController.Edit), x => new {id = x.Data.Id})
            .RequireRoutedLink(nameof(PostsHateoasController.Delete), nameof(PostsHateoasController.Delete), x => new {id = x.Data.Id})
            .RequireRoutedLink(nameof(PostsHateoasController.Patch), nameof(PostsHateoasController.Patch), x => new {id = x.Data.Id});
    });
});

Here you need to call a RequiredRoutedLink for every endpoint in your controller in this case we are supporting GET, GET(id), POST, PUT, DELETE & PATCH.

Test your web API and inspect the new Response

Now you are ready to go and take your API for a test and you will be able to see the changes right away on the Get and Get(id) methods as these are the only two methods that actually return something different to a NoContent response as the other three methods.

Get Post HATEOAS Response

Here you can appreciate the response for a single post, the Get all posts will return a much much larger response and to save some space I will not post an example as you can figure out how it will look or you can run your own test.

{
  "data": {
    "id": 13,
    "title": "Github Basics",
    "body": "New Body",
    "createdDate": "0001-01-01T00:00:00",
    "author": {
      "id": 5,
      "name": "Oscar Montenegro"
    },
    "tags": [
      {
        "id": 1,
        "name": "My first Tag",
        "description": "This is the first Tag"
      },
      {
        "id": 2,
        "name": "Programming",
        "description": "Topics related to programming"
      },
      {
        "id": 4,
        "name": "DevOps",
        "description": "Learn the latest DevOps trends and news"
      }
    ]
  },
  "links": {
    "Get": {
      "rel": "PostsHateoas/Get",
      "href": "<http://localhost:5049/api/hateoas/posts>",
      "method": "GET"
    },
    "GetById": {
      "rel": "PostsHateoas/GetById",
      "href": "<http://localhost:5049/api/hateoas/posts/13>",
      "method": "GET"
    },
    "Edit": {
      "rel": "PostsHateoas/Edit",
      "href": "<http://localhost:5049/api/hateoas/posts/13>",
      "method": "PUT"
    },
    "Delete": {
      "rel": "PostsHateoas/Delete",
      "href": "<http://localhost:5049/api/hateoas/posts/13>",
      "method": "DELETE"
    },
    "Patch": {
      "rel": "PostsHateoas/Patch",
      "href": "<http://localhost:5049/api/hateoas/posts/13>",
      "method": "PATCH"
    }
  }
}

Conclusion

Implementing HATEOAS in our ASP NET Core Web APIs unlocks a wealth of benefits, including improved discoverability, enhanced navigability, and a more intuitive client experience. By embracing the principles of HATEOAS and leveraging hypermedia links, we empower clients to dynamically explore and interact with our APIs.

As you continue to develop your ASP NET Core Web APIs, consider incorporating HATEOAS to create more robust and user-friendly interfaces. By embracing this architectural principle, you'll be able to build APIs that provide seamless navigation, decoupled interactions, and a more adaptable design.

Almost Hitting 5K monthly views

Unit Coding
Learn software development creating fun & amazing projects and get professional developers advice on life and work.

I can’t hide my happiness as this has been an amazing week full of work, reaching milestones, and planning new content for your guys I enjoy to be constantly learning something new to share with this beautiful community. I can’t help but thank you all for your support this helps me to create even more amazing and quality content for you as I know you have been enjoying reading this series and we are reaching the end of it, I’m so excited to create a grand finale and to continue with more great content. Please help me sharing this on your networks and with other fellow developers that are still learning or that have time developing but that you think they will get value from reading this articles. Thank you guys for everything, have a great day and happy coding!