Reducing duplicate code in our applications using HATEOAS

info

This blog post is a companion to my Learning to ❤ HATEOAS talk! If you are unable to visit a session of this talk, you can read this blog post instead! This way as many people as possible can learn about the power of HATEOAS!

My talk offers some more in-depth information, so if you want to know more, take a look at my Speaking page to see when and where I will be giving this talk again! I also talked about HATEOAS and the contents of this blog post on a BetaTalks podcast episode! Click here to learn more about this!

Introduction

As developers, we like to prevent duplicate code. After all, it’s one of those core principles we like to follow called DRY: Don’t repeat yourself. If you have lots of duplicate code in your codebase, at some point you will forget to update all instances of it which will result in annoying bugs.

In this blog post I will show you that all of our web apps still contain lots of duplicate code that constantly slow us down. You just don’t realize it (yet!). And I will show you how this can be solved with the power of HATEOAS!

The problem: Client-side business logic

This duplicate code is something I like to call client side business logic. (Almost) every application has client side business logic! Think of a webshop that has a minimum order amount. That amount is a piece of business logic that we want to use when someone is trying to confirm their order; did the user really meet that minimum amount? If not, we will throw an error, display a message, etc…

Other examples of client-side business logic:

  • Facebook only showing a “remove friend” button if you are friends with the other person.
  • Whatsapp only showing an “add user” button in a group chat when the user is an administrator and the group isn’t full.
  • Deciding to only render a WYSIWYG editor in a comment section when the post accepts new comments.
  • Lots of other generic add/edit/delete functionality.

The thing that all these examples have in common is that there needs to be some code that decides if certain actions are allowed. This is (action) validation/business logic, and always has to happen on the server.
But we also want this code in the front-end so we can decide if the relevant buttons/links should be shown or not!

This is duplicate code.

The importance of server-side validation

Why do we always want the validation on the server? Well, a user could easily work around a scenario where we would only validate in the front-end that the minimum order amount has been reached. For example, the user can change the validation code or disable JavaScript altogether! Non-javascript workarounds also exist, like using curl or postman to talk to the API directly.

This results in the user being able to place a €2 order even though the actual minimum amount is €10!

As you can imagine, this is a problem every application can have. The way you fix this is by always doing validation on the server. The user can’t change our back-end code or work around it, making it a good and natural place to perform these sort of checks.

But what if this minimal amount changes to €15? Then we have to update our client and server.

Why is this such a big problem?

I would argue that having to update the client and server is undesirable. We have to write roughly the same code multiple times and test and release both platforms. It’s a waste of time and money. And let’s not forget that duplicate code will get out of sync over time, causing bugs.

To really nail down the problem we are talking about here, let’s take a simplified version of Twitter as an example. Imagine that you are tasked to implement a feature where deleting a tweet is only allowed if there are less than 1000 retweets. More than 1000 retweets means the tweet is too popular for deletion.

Let’s take a look at some example code for this scenario. We are looking at some code of a simple Twitter SPA that decides if a delete button should be shown to the user or not. This example is written in (pseudo) Angular. I kept it as basic as possible so everyone should be able to understand these examples.

<!-- Client (Angular) -->
<delete-tweet-button *ngIf="tweetCanbeDeleted()"></delete-tweet-button>
// Client (TypeScript)
tweetCanBeDeleted(): boolean {
    return this.tweet.userId === this.currentUserId && this.tweet.retweets < 1000;
}

I am a big fan of C#, so let’s say our server is written in C#. As discussed above, our back-end should also validate if deletion is allowed. This results in this bit of code being called to check if deletion is allowed:

// Server (C#)
public bool TweetCanBeDeleted(int currentUserId)
{
    return tweet.UserId == currentUserId && tweet.Retweets < 1000;
}

But Twitter doesn’t just have a client and server. They have apps for Web, Windows, Android and iOS. So we might be talking about multiple teams having to update some business logic that then needs to be tested, released, etc.. And you can imagine that this business logic code looks very similiar on all of the platforms!

And that’s not all! You also want to release the updates to all platforms at the same time. Meaning that any hiccups in the release process (like ToS changes on the Apple platform) causes delays for all clients. That’s a lot of work that costs a lot of time and money for such a simple change!

Third-party platforms

And then there’s the problem of third-party platforms. Big platforms often expose an API so developers can build their own apps or functionality on top of the existing platform.

Twitter has a lot of third-party platforms that use their API. For example, alternative Twitter apps like Owly or Talon. These third-party apps can’t be updated by Twitter, so when Twitter releases their new feature, a tweet with 50000 retweets will still show a delete button, resulting in an error on the server. This can be very frustrating for the developers of those apps that constantly need to update their apps to support Twitter’s new business logic. It can also result in Twitter slowing down their releases to give third-party apps time to prepare for the update.

So, how do we update all our clients without updating them?

With the power of ✨HATEOAS✨!

Explaining HATEOAS

HATEOAS is a part of the REST API standard, although you don’t see it implemented often.

With HATEOAS, a client interacts with a network application whose application servers provide information dynamically through hypermedia. A REST client needs little to no prior knowledge about how to interact with an application or server beyond a generic understanding of hypermedia..
Wikipedia1

Wikipedia’s explanation of Hypermedia is pretty advanced. I like to explain Hypermedia as a way to link some information to other relevant information or actions. Let’s take a look at some examples.

Imagine a basic API response of a tweet in JSON. For example, the first ever public tweet on the platform:

Without HATEOAS:

{
  "id": 20,
  "userId": 12,
  "text": "just setting up my twttr",
  "favorites": 163601,
  "retweets": 500
}

With HATEOAS:

{
  "id": 20,
  "userId": 12,
  "text": "just setting up my twttr",
  "favorites": 163601,
  "retweets": 500,
  "links": [
    {
      "href": "api/tweets/20",
      "rel": "delete",
      "type": "DELETE"
    },
    {
      "href": "api/tweets/20/retweet",
      "rel": "retweet",
      "type": "POST"
    },
    {
      "href": "api/tweets/20/favorite",
      "rel": "favorite",
      "type": "POST"
    },
  ]
}

A link contains an href property which is the relative/absolute URL where you should perform an HTTP call to. The type property specifies which HTTP method should be used. Finally, you have the rel property which is the human readable explanation of this action.

This is just a subset of possible links. You could also imagine a reply link, a user link to retrieve more info about the user, a list link back to the list of tweets, etc..
It’s important to realize that these links should not be static! A delete link should only be added if the user is allowed to do so, and if the object is in the right state for deletion. For example, the server would not return a delete link if I were to view this tweet because I am not the owner.

Solving the problem

Now that you know the basics about HATEOAS, let’s take a look at how HATEOAS solves our problem.

tweetCanBeDeleted(): boolean {
    return this.tweet.links.some((x) => x.rel === 'delete');
}

… Have I blown your mind yet? Because our front-end now doesn’t need any more business logic for tweet deletion! Thanks to HATEOAS we only have to check if there is a delete link returned by the API. If a tweet would get 1000 retweets and you would ask the server for that tweet, the delete link would no longer be returned.

This works because the API is now also returning the actions you are allowed to perform on the data. The front-end no longer needs this business logic because the server tells the client that a delete action is allowed. Any changes to your business logic in the API are instantly reflected in your front-end; you only need to update, test and release your back-end.

And if that doesn’t blow your mind, consider the fact that this works for every platform. We are only checking if a specific property exists in an array. This is of course also supported on iOS, Android, all kinds of desktop applications, etc.. That makes this so powerful! You could even use this for server-to-server communication!

So, to summarize: you can now update all your clients without updating them! Just update your business logic, which causes your links to be returned more/less often, which results in your applications being updated immediately! Go take a look at all those examples of business logic I mentioned at the beginning of this post, and realize that all of them can be fixed by utilizing HATEOAS!

Implementing HATEOAS

Now that you know what HATEOAS is and how amazing it is, let’s spend some time discussing on how you can actually get started with it!

The simple way

The simple way is.. well, very simple! Let’s take a look at a pseudo example of a very small ASP.NET Core minimal API application written in C#:

var app = WebApplication.CreateBuilder(args).Build();

app.MapGet("api/tweets/{id}", async (TweetDatabase database, int id) =>
{
    var tweet = await database.GetTweetAsync(id);
    
    // This is the important part!
    if (tweet.CanBeDeleted())
    {
        tweet.Links.Add(new HateoasLink("delete", $"api/tweets/{id}", HttpMethod.Delete));
    }

    return tweet;
});

app.Run();

It’s basically 3 lines of code! The tweet we are returning to the client has a Links property which is an array of HateoasLink objects, each containing rel, href, and type properties.

With that in mind, all we need to do is check if the tweet can be deleted. If so, we add a new HateoasLink to the list with the correct information. Adding other links or using different conditions can now easily be done!

Using a library

The previous examples is very simple to understand, but also has downsides like lots of if statements all over the place, and poor testability. We can use a library for HATEOAS to improve the code quality.

During Hacktoberfest 2021 I worked on open-sourcing the HATEOAS library we use internally at my employer Arcady. This is a library written in C# written for the ASP.NET Core MVC framework. You can find it on GitHub with the name munisio. At the time of writing this is still a work in progress, but I still think it’s worth highlighting here!

At the bottom of this post you can find links to libraries for other languages/frameworks!

// https://github.com/arcadyit/munisio (MIT license)
public class TweetHateoasProvider : IAsyncHateoasProvider<GetTweetResponseModel>
{
    private readonly ITweetRepository _tweetRepository;

    public TweetHateoasProvider(ITweetRepository tweetRepository)
    {
        _tweetRepository = tweetRepository;
    }
    public async Task EnrichAsync(IHateoasContext context, GetTweetResponseModel model)
    {
        var tweet = await _tweetRepository.GetByIdAsync(model.Id);

        await model
            .AddDeleteLink("delete", $"api/tweets/{model.Id}")
            .When(() => tweet.CanBeDeleted())
            .WhenAsync(() => context.AuthorizeAsync(tweet, Operations.Delete));
    }
}

This example is a bit more advanced than the previous one, and to fully understand some parts you will need some deeper knowledge of ASP.NET Core’s middleware system. Because I don’t think this is relevant for this blog post, we will simply focus on the EnrichAsync function instead.

This class contains all the HATEOAS logic for the GetTweetResponseModel, an object we will return when a user requests a specific tweet. When we are done with retrieving the tweet from the database and are about to send it back to the client, EnrichAsync grabs the model we are returning and calls multiple methods on it like AddDeleteLink() to dynamically add links to it. This is only done if the tweet can be deleted and if the user is authorized to do so.

This is a big improvement compared to the previous example. This class is easily testable, links can be setup dynamically with methods like When() and the class itself can be extended with other links without breaking other bits of code. And that’s just the top of the iceberg; lots of other cool features (like automatic link generation) are planned!

Feel free to check this library out! Stars and contributions are welcome, and I am curious to hear what you think!

Using a library in the front-end

I quickly want to touch upon using HATEOAS in the front-end world. As you saw earlier, it’s as easy as checking if an item exists in the links array. In my opinion you don’t really need a library for this.

But in my Angular projects I have the following directive to make it even easier to work with HATEOAS. With this directive you can keep your HATEOAS checks in your HTML, reducing the amount of TypeScript functions you need to write. It looks like this:

<delete-tweet-button *hateoas="{ rel: 'delete', links: tweet.links }"></delete-tweet-button>

The source of the directive:

@Directive({
  selector: '[hateoas]',
})
export class HateoasDirective {
  @Input() set hateoas(data: { rel: string; links: { href: string; rel: string; method: string }[] }) {
    this._rel = data.rel;
    this._links = data.links;

    this.setNgIf();
  }

  private _ngIfDirective!: NgIf;
  private _links!: { href: string; rel: string; method: string }[];
  private _rel!: string;

  constructor(
    private readonly _templateRef: TemplateRef<NgIfContext<unknown>>,
    private readonly _viewContainer: ViewContainerRef,
  ) {}

  private setNgIf(): void {
    if (this._ngIfDirective == null) {
      this._ngIfDirective = new NgIf(this._viewContainer, this._templateRef);
    }

    this._ngIfDirective.ngIf = this._links.some((x) => x.rel.toLowerCase().trim() === this._rel.toLowerCase().trim());
  }
}

You could easily port something like this to any other framework or platform. Let me know what you come up with!

The ups and downs of HATEOAS

This part of the blog post might be a bit.. controversial. That is because it touches upon the pain points of HATEOAS. For example, you might have noticed that we haven’t used the type and href properties at all so far. It’s easy to explain why: we simply haven’t needed them yet because they are not immediately useful to us. And I believe that is also exactly one the reasons why HATEOAS never really caught on:

  • HATEOAS specifies the HTTP method and URL of the action, but a schema for validation or the request body is not part of the standard. This means you still need to have some knowledge of the server’s expectations, defeating the original purpose of HATEOAS. I asked a question on StackOverflow about this but I haven’t found a satisfying solution for this problem yet.
    • In any case, this is already a bit of solved problem. Nowadays we use graphql and generated API clients to make communication with the server easier.
    • Although.. It is worth mentioning that the combination of these properties can be useful! I like using a download-file link to render a link on a page where the user can download a file. The <a href> will then contain the link returned from the API.
  • HATEOAS aims to decouple the client and server further, but the client still needs to know the rel to do useful things. Meaning that the coupling doesn’t fully go away. I do wonder if that bit of coupling actually matters, though.

In any case, I do believe that these downsides don’t weigh up against the upsides of using HATEOAS to remove lots of duplicate code and business logic from our applications.

Alternatives

If you believe HATEOAS doesn’t fit your situation, I can still tell you of a way to get the exact same benefits without HATEOAS!
The important thing is that our server tells our clients what actions can be performed on the object. So you could return something like this from your API:

{
  "id": 20,
  "userId": 12,
  "text": "just setting up my twttr",
  "favorites": 163601,
  "retweets": 500,
  "actions": [ "delete", "retweet" ]
}

I would not recommend this, though. HATEOAS is a standard that you can implement. That results in any client being able to easily parse the hypermedia into a common, well-known format. If you would simply create an array of actions, you would create something custom which would be more work for clients to implement!

Finishing up

Do you want to learn more about HATEOAS? Take a look at these links below, or go to my speaking page to see when and where I am going a talk about HATEOAS!

Take a look at these libraries to get started with HATEOAS:

And finally, while I was writing this blog post, I saw a great blog post about REST and HATEOAS on Hacker News. Click here to read it!


  1. The above quote is excerpted from Wikipedia’s HATEOAS article ↩︎