How to implement deep linking client on top of HATEOAS server?
Asked Answered
N

3

14

There's a similar question on SO, but it's not phrased well and it lacks details. So I'm trying to write a better question.

I'm interested in how to implement HATEOAS with a single page application (SPA) that is using pushState. I want to preserve deep linking so that users can bookmark URLs within the SPA and revisit them later or share them with other users.

For concreteness, I'll present a hypothetical example. My single page application is hosted at https://www.hypothetical.com/. When a user visits this URL in a browser, it downloads an SPA and bootstraps. The SPA looks at the browser's current location.href in order to figure out what API resource to fetch and render. In the case of the root URL, it requests https://api.hypothetical.com/, which renders a response like this:

{
  "employees": "https://api.hypothetical.com/employees/",
  "offices": "https://api.hypothetical.com/offices/"
}

I'm glossing over some details like accept and content-type, but let's assume that this hypothetical API supports content-negotiation and other RESTful goodness.

Now the SPA renders a user interface that displays these two link relations to the user and the user can click an "Employees" button to view employees or "Offices" to view offices. Let's say the user clicks "Employees". The SPA needs to pushState() some new href, otherwise this navigation decision will not appear in the history and the user will not be able to use the Back button to return to the first screen.

This presents a small dilemma: what href should the SPA use? Clearly, it can't push https://api.hypothetical.com/employees/. Not only is that not a valid resource within the SPA, its not even in the same origin and pushState() throws exceptions if the new href is in a different origin.

The dilemma is [perhaps] easily resolved: the SPA is aware of a link relation called employees, so the SPA can hard code a URL for this resource: pushState(...,'https://www.hypothetical.com/employees'). Next, it uses the link relation https://api.hypothetical.com/employees/ to fetch an employee collection. The API returns a result like this:

{
  "employees": [
    {
      "name": "John Doe",
      "url": "https://api.hypothetical.com/employees/123",
    },
    {
      "name": "Jane Doe",
      "url": "https://api.hypothetical.com/employees/234",
    },
    ...
  ]
}

There are more than two results, but I've abbreviated with an ellipsis.

The SPA wants to displays this data in a table where each employee name is a hyperlink so that the user can view more details about a specific employee. The user clicks on "John Doe". The SPA now needs to display details about John Doe. It can easily obtain the resource using the link relation, and it might get something like this:

{
  "name": "John Doe",
  "phone_number": "2025551234",
  "office": {
    "location": "Washington, DC",
    "url": "https://api.hypothetical.com/offices/1"
  },
  "supervisor": {
    "name": "Jane Doe",
    "url": "https://api.hypothetical.com/employees/234"
  },
  "url": "https://api.hypothetical.com/employees/123"
}

But now the same dilemma rises again: what URL should the SPA choose to represent this new internal state? This is the same dilemma as above, except this time it's not possible to hardcode a single SPA URL, because there are an arbitrary number of employees.

I think this is a non-trivial question, but let's do something hacky so we can keep moving forward: the SPA constructs a URL by replacing 'api' in the hostname with 'www'. It's awfully ugly, but it doesn't violate HATEOAS (the SPA URL is only used client side) and now the SPA can pushState(...,'https://www.hypothetical.com/employees/123'. Generalizing this approach, the SPA can display navigation options for any link relation, and the user can explore related resources: where is this person's office? What is the supervisor's phone number?

This still doesn't solve deep linking. What if the user bookmarks https://www.hypothetical.com/employees/123, closes the browser, and then revisits this bookmark later on? Now the SPA has no recollection of what the underlying API resource was. We can't reverse the substitution (e.g. replace 'www' with 'api') because that's not HATEOAS.

The HATEOAS mindset seems to be that the SPA should request https://api.hypothetical.com/ again and follow links back to John Doe's employee profile, but there's no fixed link relation to get from employees as a collection to John Doe as a specific employee, so that won't work.

Another HATEOAS approach is that the application could bookmark URLs that it has discovered. For example, it could store a hash table that maps previously seen SPA URLs to API URLs:

{
  "https://www.hypothetical.com/employees/123": "https://api.hypothetical.com/employees/123"
}

This would allow the SPA to find the underlying resource and render the UI, but it requires persistent state across sessions. E.g. if we store this hash in HTML5 storage and the user clears their HTML5 storage, then all of the bookmarks would break. Or if the user sends this bookmark to another user, that other user wouldn't have this mapping of SPA URL to API URL.

Bottom line: implementing a deep linking SPA on top of a HATEOAS API feels very awkward to me. In my current project, I've resorted to having the SPA construct almost all of the URLs. As a consequence of that decision, the API must send unique identifiers (not just URLs) for individual resources so that the SPA can generate good URLs for them.

Does anybody have experience doing this? Is there a way to satisfy these seemingly contradictory criteria?

Narbada answered 13/3, 2015 at 19:24 Comment(1)
Six years later I'm struggling with the same problem. There are not good example-implementations on the web. Did you solve your problem meanwhile? Do you have any reference or tutorials for your solution?Humorist
L
0

I'm probably missing something, but presumably for different resource types you have different views/pages. So when you load employees it uses the employees.html view and when you get to an employee you use the employee.html view..so why woudl it' be more like that

//user clicks employees
pushState( "employees.html#https://api.hypothetical.com/employees/" )

//user clicks on an employeed
pushState( "employee.html#https://api.hypothetical.com/employees/12345/" )

i mean URI component encode the url for sure...but now you have deeplinks to views of resources, no hack required.

I see pushState but really it's probably more accurately thought of as pushViewState

Lusterware answered 2/4, 2015 at 6:26 Comment(4)
if you have different pages for different resources, it's no longer a SPAConjuncture
I'm guessing he meant partials or routes, so it is SPA.Lyallpur
Having a URL inside of another URL looks kind of ugly, imho. But looks like it's the best solution so far.Aguedaaguero
pretty urls has nothing to do with RESTfulLusterware
A
5

I have struggled with this concept as well. A hateoas api is all about decoupling the client from the server, so I think hardcoding a map of "client url -> api uri" into local storage is a step backwards for the reasons you mentioned and more. That being said, there's nothing coupling about having separate domain languages for the client and the server. You can have certain actions on the page (such as clicking employee # _) trigger push state with whatever you like, perhaps '/employees/#'. When the app is bootstrapped into another browser by link-sharing, some service will know how to read the DSL '/employees/#' and fast forward your app into the appropriate state. In the instance where permissions are different, you have some message explaining why you can't access employee # _, and instead list the employees you do have permission to see. It can get pretty cumbersome if you have a lot of deep linking views like this, but such is the price of representing a complex state object with a flat url hierarchy.

Another approach I have considered is an explicit "share this" button. Clicking the button will ask the server to mint a url in its own DSL, so that when another user visits this share url, the server can transfer the relevant state information to the client. The client must be setup to handle this scenario, perhaps with some conditionals around "if this resource contains a 'deep link' property, do something special.

Either way it's not an easy problem to solve.

Acaricide answered 18/7, 2015 at 17:0 Comment(1)
Almost two years later: any new thoughts on this? I'm a newbie to REST and am having this same issue, where it's easy enough building a client that follows links deeper into a server API, but a deep link on the client seems like it would have to know about the server's URL scheme, or there would have to be some kind of 3rd "deep link cache" service just to solve this problem...Salahi
L
0

I'm probably missing something, but presumably for different resource types you have different views/pages. So when you load employees it uses the employees.html view and when you get to an employee you use the employee.html view..so why woudl it' be more like that

//user clicks employees
pushState( "employees.html#https://api.hypothetical.com/employees/" )

//user clicks on an employeed
pushState( "employee.html#https://api.hypothetical.com/employees/12345/" )

i mean URI component encode the url for sure...but now you have deeplinks to views of resources, no hack required.

I see pushState but really it's probably more accurately thought of as pushViewState

Lusterware answered 2/4, 2015 at 6:26 Comment(4)
if you have different pages for different resources, it's no longer a SPAConjuncture
I'm guessing he meant partials or routes, so it is SPA.Lyallpur
Having a URL inside of another URL looks kind of ugly, imho. But looks like it's the best solution so far.Aguedaaguero
pretty urls has nothing to do with RESTfulLusterware
E
0

It is up to the client to determine which URLs it uses; they don't have to (and shouldn't) match the server's URLs. I have detailed how and why in another answer: How can I avoid hardcoding URLs in a RESTful client/server web app with deep linking?

As long as the client can determine the resource type and the identifier it can query the server for the URL it needs to retrieve the resource.

Elbowroom answered 22/11, 2022 at 10:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.