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