📜 ⬆️ ⬇️

Problem of deep links in HATEOAS

External linking (deep linking) - on the Internet, this is a hyperlink to a site that points to a page located on another web site, instead of pointing to the starting (home, start) page of that site. Such links are called external links (deep links).
Wikipedia
The term “deep links” will be used further, as the closest to the English-language “deep links”. Speech in this article will be about REST API, so the deep links will be meant links to HTTP-resources. For example, the deep link habr.com/ru/post/426691 points to a specific article on the site habr.com.

HATEOAS is a component of the REST architecture that allows you to provide API information to clients via hypermedia. The client knows the only fixed address, the API entry point; he learns all possible actions from the resources received from the server. Resource views contain links to actions or other resources; the client interacts with the API by dynamically selecting an action from the available links. You can read more about HATEOAS on Wikipedia or in this wonderful article on Habré.

HATEOAS is the next level of REST API. Thanks to the use of hypermedia, it answers many questions that arise when developing an API: how to control access to server-side actions, how to get rid of tight connectivity between the client and the server, how to change the addresses of resources if necessary. But he does not provide an answer to the question of how deep links to resources should look.

In the "classic" implementation of REST, the client knows the address structure, he knows how to get a resource in the REST API by identifier. For example, a user follows a deep link to a book page in an online store. The URL of the browser displays the URL https://domain.test/books/1 . The client knows that “1” is the resource identifier of the book, and to obtain it, you must substitute this identifier in the REST API URL https://api.domain.test/api/books/{id} . Thus, the deep link to the resource of this book in the REST API looks like this: https://api.domain.test/api/books/1 .
')
In HATEOAS, the client does not know about resource identifiers or address structure. It is not hardcoded, but “discovers” links. Moreover, the structure of URLs can change without the knowledge of the client, HATEOAS allows it. Because of these differences, it will not be possible to implement deep links in the same way as the classic REST API. Surprisingly, the Internet search for recipes for the implementation of such links in HATEOAS did not give a large number of results, only a few puzzled questions on Stackoverflow. Therefore, we consider several possible options and try to choose the best.

Zero option out of competition - do not implement deep links. This may be suitable for some admins or mobile applications that do not require the possibility of a direct transition to internal resources. It is completely in the spirit of HATEOAS, the user can open pages only sequentially, starting from the entry point, because the client does not know how to go to the internal resource directly. But this option is poorly suited for web applications - we expect that the link to the internal page can be bookmarked, and updating the page will not take us back to the main page of the site.

So, the first option: the hardcode URLs of the HATEOAS API. The client knows the structure of resource addresses for which deep links are needed, and knows how to get the resource identifier for the substitution. For example, the server returns the address https://api.domain.test/api/books/1 as a link to the book resource. The client knows that “1” is the book identifier and can form this URL on his / her own when clicking on the depth link. This is certainly a working option, but violates the principles of HATEOAS. The structure of the address and the identifier of the resource cannot be changed, otherwise the client will break, there is a rigid connectivity. This is not HATEOAS, which means that the option does not suit us.

The second option is to substitute the REST API URL into the client URL. For the book example, the deep link will look like this: https://domain.test/books?url=https://api.domain.test/api/books/1 . Here the client takes the resource link received from the server and substitutes it entirely in the address of the page. This is more like HATEOAS, the client does not know about the identifiers and the address structure, he gets the link and uses it as is. When clicking on such a deep link, the client will get the necessary resource via the REST API link from the url parameter. It would seem that the solution is working, and quite in the spirit of HATEOAS. But if you add such a link to your bookmarks, in the future we will not be able to change the address of the resource in the API (or we will have to always keep forwarding to a new address). Again, one of the advantages of HATEOAS is lost, this option is also not perfect.

Thus, we want to have permalinks, which, however, may change. Such a solution exists and is widely used on the Internet — many sites provide short links to internal pages that can be shared. In addition to brevity, their advantage is that the site can change the real address of the page, but such links will not break. For example, Microsoft uses in Windows links to help pages like http://go.microsoft.com/fwlink/?LinkId=XXX . Over the years, the Microsoft sites have been reworked several times, but the links in older versions of Windows continue to work.

It remains only to adapt this solution to HATEOAS. And this is the third option - the use of unique identifiers of deep references in the REST API. Now the address of the page with the book will look like this: https://domain.test/books?deepLinkId=3f0fd552-e564-42ed-86b6-a8e3055e2763 . When clicking on such a deep link, the client should ask the server: which link to the resource corresponds to such a deepLinkId identifier? The server will return the https://api.domain.test/api/books/1 (or the resource at once, so that it does not go twice). If the address of the resource in the REST API changes, the server simply returns another link. A record is saved in the database that the reference identifier 3f0fd552-e564-42ed-86b6-a8e3055e2763 corresponds to the entity identifier of book 1.

To do this, resources must contain a deepLinkId field with identifiers of their deep links, and the client must substitute them into the page address. Such an address can safely be bookmarked and sent to friends. It’s not at all good that the client works independently with certain identifiers, but this allows you to retain the advantages of HATEOAS for the API as a whole.

Example


This article would be incomplete without an example implementation. To test the concept, consider an example of a site directory of a hypothetical online store with a Spring Boot / Kotlin backend and a SPA frontend on Vue / JavaScript. The store sells books and pencils, the site has two sections in which you can view the list of products and open their pages.

Book section":



Page of one book:



Spring Data JPA entities are defined for storage of goods:

 enum class EntityType { PEN, BOOK } @Entity class Pen(val color: String) { @Id @Column(columnDefinition = "uuid") val id: UUID = UUID.randomUUID() @OneToOne(cascade = [CascadeType.ALL]) val deepLink: DeepLink = DeepLink(EntityType.PEN, id) } @Entity class Book(val name: String) { @Id @Column(columnDefinition = "uuid") val id: UUID = UUID.randomUUID() @OneToOne(cascade = [CascadeType.ALL]) val deepLink: DeepLink = DeepLink(EntityType.BOOK, id) } @Entity class DeepLink( @Enumerated(EnumType.STRING) val entityType: EntityType, @Column(columnDefinition = "uuid") val entityId: UUID ) { @Id @Column(columnDefinition = "uuid") val id: UUID = UUID.randomUUID() } 

To create and store identifiers of deep references, the DeepLink entity is DeepLink , an instance of which is created with each domain object. The identifier itself is generated according to the UUID standard at the time the entity is created. Its table stores the identifier of the deep link, the identifier and the type of the entity being referenced.

The REST API of the server is organized according to the HATEOAS concept, the API entry point contains links to collections of goods, as well as the #deepLink link for the formation of deep references by identifier substitution:

 GET http://localhost:8080/api { "_links": { "pens": { "href": "http://localhost:8080/api/pens" }, "books": { "href": "http://localhost:8080/api/books" }, "deepLink": { "href": "http://localhost:8080/api/links/{id}", "templated": true } } } 

When opening the “Books” section, the client requests a collection of resources via the #books link at the entry point:

 GET http://localhost:8080/api/books ... { "name": "Harry Potter", "deepLinkId": "4bda3c65-e5f7-4e9b-a8ec-42d16488276f", "_links": { "self": { "href": "http://localhost:8080/api/books/1272e287-07a5-4ebc-9170-2588b9cf4e20" } } }, { "name": "Cryptonomicon", "deepLinkId": "a23d92c2-0b7f-48d5-88bc-18f45df02345", "_links": { "self": { "href": "http://localhost:8080/api/books/5d04a6d0-5bbc-463e-a951-a9ff8405cc70" } } } ... 

The SPA uses the Vue Router, for which the path to the page of the book { path: '/books/:deepLinkId', name: 'book', component: Book, props: true } , and the links in the list of books look like this: <router-link :to="{name: 'book', params: {link: book._links.self.href, deepLinkId: book.deepLinkId}}">{{ book.name }}</router-link> .

That is, when opening a page of a specific book, the Book component is called, to which two parameters are passed: link (link to the book resource in the REST API, href field value of the #self link) and deepLinkId from the resource).

 const Book = { template: `<div>{{ 'Book: ' + book.name }}</div>`, props: { link: null, deepLinkId: null }, data() { return { book: { name: "" } } }, mounted() { let url = this.link == null ? '/api/links/' + this.deepLinkId : this.link; fetch(url).then((response) => { return response.json().then((json) => { this.book = json }) }) } } 

The value of deepLinkId Vue Router sets the address of the page /books/:deepLinkId , and the component requests the resource by a direct link from the link property. When you force a refresh of the page, the Vue Router sets the property of the deepLinkId component, retrieving it from the page address. The link property remains null . The component checks: if there is a direct link obtained from the collection, the resource is requested by it. If only the deepLinkId identifier is deepLinkId , it is inserted into the #deepLink link from the entry point to get the resource via the deep link.

On the back end, the controller method for the deep links looks like this:

 @GetMapping("/links/{id}") fun deepLink(@PathVariable id: UUID?, response: HttpServletResponse?): ResponseEntity<Any> { id!!; response!! val deepLink = deepLinkRepo.getOne(id) val path: String = when (deepLink.entityType) { EntityType.PEN -> linkTo(methodOn(MainController::class.java).getPen(deepLink.entityId)) EntityType.BOOK -> linkTo(methodOn(MainController::class.java).getBook(deepLink.entityId)) }.toUri().path response.sendRedirect(path) return ResponseEntity.notFound().build() } 

The identifier is the essence of the deep link. Depending on the type of the application entity, a link is formed to the controller's method, which returns its resource by entityId . Request redirected to this address. Thus, if in the future the link to the entity controller changes, you can simply change the link formation logic in the deepLink method.

The full source code for the sample is available on Github .

Source: https://habr.com/ru/post/445092/


All Articles