简体   繁体   中英

Consuming HATEOAS API from native clients, Is REST really REST?

I'm writing Web API in ASP NET Core and I want to consume it from single page applications (eg using Angular, Vue, React), native desktop applications and mobile applications.

I stumbled across concept called "HATEOAS" and I learnt that the API I'm building isn't really RESTful and I wrongly named it RESTful ( https://devblast.com/b/calling-your-web-api-restful-youre-doing-it-wrong ).

And it seems like most of the people use this term badly (Roy T. Fielding - man behind REST idea about his annoyance: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven )

From what I learnt the idea behind HATEOAS is thinking of your application as you think of websites, that means it exposes links to resources (like <a href.../> in HTML) so you don't have to hardcode the links/endpoints in your client code and your client code won't break if you change some endpoint (?).

Another thing is that it makes your API discoverable without any documentation, ie API describes itself (like meta API)

When I look at the existing "REST" clients for C# for example:

They have "REST" in their name but none of them dig into the concept of HATEOAS. So why the heck are they named "Rest clients"?

How HATEOAS is supposed to be used on the client-side? To my surprise there isn't much about it on the internet, there's a lot about how to implement HATEOAS on the server, but there isn't much how it's supposed to work on the client. That most often should provide navigation logic.

Besides of that all, there are many API client generators (parsing OpenAPI specification) like AutoRest (nothing about hyperlinks and HATEOAS, to my surprise), or NSwag

I was googling for few hours to learn about HATEOAS, but most often people talk about it without describing how to use it (there are almost none client libraries supporting it).

There are many standards for it like HAL , Ion etc. But there are almost none rest client libraries implementing those standards. There's also json:api . All these standards are very similar.

So my question is:

  • Is HATEOAS applicable for applications like SPAs, mobile and desktop clients?

  • What's the real use case for HATEOAS, if I can as well hardcode my endpoints, or generate new API client from OpenAPI (Swagger) specification as it changes?

  • Is it even worth to bother with it?

There are almost none practical examples of interacting with HATEOAS or Hypermedia APIs, or am I missing something here and they're not supposed to be used by my client code?

To me it seems like implementing it on both client and the server is a lot of boilerplate code, so why aren't there many libraries supporting it out of the box (using one of the standard). It seems like json:api has many implementations http://jsonapi.org/implementations/#client-libraries-net

REST is a paradigm that applies more broadly than just the HTTP protocol and so-called "web APIs". A RESTful Web API, is one that simply applies principles from the REST paradigm to client-server communication over HTTP. As such, it doesn't necessary follow everything in REST and doesn't necessarily need to.

While HATEOAS is a nice concept, there's no HTTP client that actually implements it out of the box (at least that I'm aware of). You can feel free to make your API implement it, but that doesn't mean it will actually be used, and while your API may be "RESTful", it doesn't mean every client will be. Part of the foundation of the HTTP protocol is adaptive communication. In other words, a client need not support all the features of a server and vice versa. The client and server, instead, work with what they share in common in terms of functionality.

Sorry, very late to this but we have attempted to implement a HATEOAS solution for an angular SPA with a .NET Core backend. We looked for out of the box solutions but ended up rolling our own. Simplistically we just returned http links from each request in a standard format that our UI would understand. They looked a bit like this:

"links": [
    {
        "title": "A thing I want to do",
        "href": "http://localhost:12345/SomeGuff/t703g176-4546-4345-643c-6615b4f166ec/tokenrenewals",
        "rel": "mybff:afunction",
        "display": "A thing I want to do",
        "method": "POST",
        "mediaTypes": [
            "application/json"
        ]
    }
]

So every response would return an array/enumerable/list of these in a standard format. The rel became the key. It was the contract between the UI and the BFF. The UI would map the rel to an angular route and deal with what to do. In simplistic terms this works really well. The backend (BFF) just returns an array of links which the UI honors.

Here is a bit more of the story:

Backend

We went with a attribute based system that allowed the controllers to be decorated with details that said they wanted to play the game.

[HttpPost]
[Route("{id}/someguff", Name = "MyRouteName")]
[Consumes("application/json")]
[Produces("application/foo-token-1.0.0+json")]
[Links("Some guff", "mybff:myfunction", "I am doing some guff")]

The allowed a service to interrogate the controller(s) and work out what to return. So the attribute on the controller had a name and a title, description etc and the caller would say to the service "give me the link for link {name} with params { new { id ="x" } or whatever the params needed to be. The service returned a bunch of pre-populated links the U just uses them (based on an angular contract)

var links = this.controllerMethodInformationService.BuildLinks(
    LinksBuilder.Create()
        .IfLinkNeeded(() => true)
            .AddLinkRequest(LinkRequestBuilder.BuildLinkRequestForId(Constants.RouteName.MyRouteName, request.Id))
        .Build());

So what about PUT/POST models? Well we either let you UI deal with them (It had a GET that returned a model, use that sort of thing). Or we added a form element that allowed the BFF to describe the model the PUT/POST expects.

And what about querystring params? Well when they are BFF derived, it worked fine. We just sent them as part of the link. But what is the querystring was part of what the UI was doing? Well that was usually a conversation between the UI/BFF devs to agree a solution! Most of the time the raw link was returned and the UI appended the extra querystring params

The ideal was to let the UI just respond to the BFF(s). Be dumb. I think it worked(s) 95% of the time. We have been able to add a BFF logic (say it is Tuesday, don't show the link) and the UI, because it does not get the link does not display the button. Also, in the BFF(s) we could aggregate our permissions and even when you asked for a link, if you did not have the permission(s) it would not be returned.

Some of the BFF work is reflection/swagger type stuff but generally the business features do not care. They just say "return this link" and it all just works.

Also, we use Mediatr and our links are added as a handler that runs after our business handler(s). Don't implement it, cool, empty links returned. Just implement an interface and the Mediatr handler will run your code. This is all part of our CQRS (Mediatr) pipleline.

Application Navigation

All the above was talking about page interactions. What about application navigation? Well we used the same process (attributes) and a special BFF that would call all the other BFFs and say "what are you endpoints"?

[NavigationLanding("SomeContext", 1, "This is a description of the context")]

This is saying "I want to be shown on the menu" (if you have the correct permission and other logic works).

A navigation endpoint in each BFF would run and all the permission/business logic would stuff apply but the nav BFF would aggregate any responses and return them to the UI.

And this was how we dealt with menus and more global application navigation! It works quite well, uses the same models/services and so far we are happy with it!

I would say, for our project, this has been one of the real plus points. It is not without its challenges and does take some work (get your backend and UI guys to converse and agree how it should work - that worked for us). When you can change a permission in the backend and suddenly a button disappears with no UI change is usually brilliant!

For most of how we identified controller endpoints etc in the BFF(s), look at how swagger/swashbuckle does it. They will point you in the correct direction :-)

I hope this helps people!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM