HATEOAS Deep Link Problem

    External linking (deep linking) - on the Internet, this is the placement of a hyperlink on a site that points to a page on another website, instead of pointing to the home (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”. This article will focus on the REST API, so deep links will mean links to HTTP resources. For example, the deep link habr.com/en/post/426691 points to a specific article on habr.com.

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

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

    In the "classic" REST implementation, the client knows the structure of the addresses; he knows how to get a resource by identifier in the REST API. For example, a user follows a deep link to a book page in an online store. URL displayed in browser address barhttps://domain.test/books/1. The client knows that “1” is the identifier of the book’s resource, and to get it you need to substitute this identifier in the URL of the REST API https://api.domain.test/api/books/{id}. Thus, the deep link to the source of this book in the REST API is as follows: https://api.domain.test/api/books/1.

    In HATEOAS, the client does not know about resource identifiers or address structure. He does not hardcode, but "discovers" links. Moreover, the structure of URLs can change without the knowledge of the client, HATEOAS allows it. Because of these differences, deep links cannot be implemented in the same way as the classic REST API. Surprisingly, an Internet search for recipes for implementing such links in HATEOAS did not yield a large number of results, only a few perplexing questions on Stackoverflow. Therefore, we will consider several possible options and try to choose the best one.

    The zero option outside the competition is not to implement deep links. This may be suitable for some admins or mobile applications that do not require the ability to directly switch to internal resources. This 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 not suitable for web applications - we expect that the link to the internal page can be bookmarked, and updating the page will not transfer us back to the main page of the site.

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

    The second option is to substitute the REST API URL in the client URL. For an example with a book, 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 page address. This is more like HATEOAS, the client does not know about identifiers and address structure, he receives a link and uses it as is. When clicking on such an in-depth link, the client will receive the desired 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 no longer be able to change the address of the resource in the API (or we will always have to redirect to a new address). Once again, one of the advantages of HATEOAS is lost; this option is also not ideal.

    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 links to view help pages in Windows http://go.microsoft.com/fwlink/?LinkId=XXX. Over the years, Microsoft sites have been redesigned several times, but links in older versions of Windows continue to work.

    It remains only to adapt this solution to HATEOAS. And this is the third option - using unique deep link identifiers 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 an in-depth link, the client should ask the server: what link to the resource corresponds to this identifier deepLinkId? The server will return the link https://api.domain.test/api/books/1(well, or immediately a resource, so as not to go twice). If the resource address in the REST API changes, the server will simply return another link. An entry was saved in the database that the link identifier 3f0fd552-e564-42ed-86b6-a8e3055e2763 corresponds to the entity identifier of book 1.

    For this, resources must contain a field deepLinkIdwith the identifiers of their deep links, and the client must substitute them in the page address. This address can be safely bookmarked and sent to friends. It’s not very good that the client independently works with certain identifiers, but this allows you to preserve the advantages of HATEOAS for the API as a whole.

    Example


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

    Books section:



    One book page:



    Spring Data JPA entities are defined for storing 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 deep link identifiers, an entity is used 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 was created. Its table contains the identifier of the deep link, the identifier and type of the entity to which the link leads.

    The server’s REST API is organized according to the HATEOAS concept, the API entry point contains links to product collections, as well as a link #deepLinkfor forming deep links by substituting an identifier:

    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
            }
        }
    }
    

    The client, when opening the "Books" section, requests a collection of resources using the link #booksat 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 Vue Router, which is defined for the path to the page of the book { path: '/books/:deepLinkId', name: 'book', component: Book, props: true }, and reference books in the list are as follows: {{ book.name }}.

    That is, when a page of a specific book is opened Book, a component is called that receives two parameters: link(link to the book resource in the REST API, hreflink field value #self) and deepLinkIdfrom the resource).

    const Book = {
        template: `
    {{ 'Book: ' + book.name }}
    `, 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 }) }) } }

    deepLinkIdVue Router sets the value to the page address /books/:deepLinkId, and the component requests the resource by direct link from the property link. When a page is forced to refresh, Vue Router sets the component property deepLinkId, retrieving it from the page address. The property linkremains equal null. The component checks: if there is a direct link obtained from the collection, the resource is requested on it. If only the identifier is available deepLinkId, it is substituted into the link #deepLinkfrom the entry point to get the resource by the deep link.

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

    @GetMapping("/links/{id}")
    fun deepLink(@PathVariable id: UUID?, response: HttpServletResponse?): ResponseEntity {
        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()
    }
    

    By identifier is the essence of the deep link. Depending on the type of application entity, a link is formed to the controller method, which returns its resource by entityId. The request is redirected to this address. Thus, if in the future the link to the entity controller changes, it will be possible to simply change the logic of link formation in the method deepLink.

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

    Also popular now: