Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

like image 619
Mark E. Haase Avatar asked Mar 13 '15 19:03

Mark E. Haase


People also ask

What is Hateoas Hal?

HAL is a simple format that gives a consistent and easy way to hyperlink between resources in your API. The HAL format is strictly coupled to HATEOAS. The main target of HATEOAS is to decouple the API Consumer from the paths used in the API.

What are Hateoas links?

1. What is HATEOAS? HATEOAS is a constraint on REST that says that a client of a REST application need only know a single fixed URL to access it. Any and all resources should be discoverable dynamically from that URL through hyperlinks included in the representations of returned resources.

What is deep linking API?

Overview. Branch's Deep Linking API is a powerful tool for all things Branch Links. You can programmatically generate links at scale to support all of your campaigns while tagging the links appropriately based on channel and other analytics tags.


1 Answers

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.

like image 188
Clev3r Avatar answered Oct 17 '22 00:10

Clev3r