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

391 views
Skip to first unread message

Mark Haase

unread,
Mar 24, 2015, 9:52:41 PM3/24/15
to api-...@googlegroups.com
(This is a cross post to Stack Overflow:
http://stackoverflow.com/questions/29040388/. If you have an account there and
would like to send an answer here and there, that would be great! If not, I
might excerpt some of the answers I receive on list and post them on Stack
Overflow.)

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:

    {
    }

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",
        },
        {
          "name": "Jane Doe",
        },
        ...
      ]
    }

*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",
      },
      "supervisor": {
        "name": "Jane Doe",
      },
    }

**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:

    {
    }

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?

Kijana Woodard

unread,
Mar 24, 2015, 10:16:13 PM3/24/15
to api-...@googlegroups.com
Why would the spa use the same url space as the api?
This would fall apart the moment a single spa view needs to access multiple api resources...much less multiple apis.



--
You received this message because you are subscribed to the Google Groups "API Craft" group.
To unsubscribe from this group and stop receiving emails from it, send an email to api-craft+...@googlegroups.com.
Visit this group at http://groups.google.com/group/api-craft.
For more options, visit https://groups.google.com/d/optout.

Mark Haase

unread,
Mar 24, 2015, 10:47:26 PM3/24/15
to api-...@googlegroups.com
I agree whole-heartedly that sharing URL spaces falls apart when a single SPA view needs to access multiple API resources. (That's a mistake I've made in the past.)


If I'm understanding the term URL space correctly, these *are* different URL spaces. That's sorta the root of the problem: what should be the mapping between these two URL spaces?

Kijana Woodard

unread,
Mar 24, 2015, 11:07:31 PM3/24/15
to api-...@googlegroups.com
You're asking, when a SPA controller is invoked, how does it "call the api" given that it can't construct urls?

My first thoughts are, how would it work without the "bookmark" case?
Is the controller being passed the url to fetch, the current resource, or the resource it will display?

Of course, it's always possible to do what desktop apps used to do: forget and start over. :-D

Will check out your SO post.

Jørn Wildt

unread,
Mar 25, 2015, 5:34:38 PM3/25/15
to api-...@googlegroups.com
You can simply include the whole API URL in the client URL - like the HAL browser does.

See for instance the FoxyCart example at https://api-sandbox.foxycart.com/hal-browser/browser.html#/ - clicking a link from that page leads to https://api-sandbox.foxycart.com/hal-browser/browser.html#https://api-sandbox.foxycart.com/reporting.

You only have to accept the idea of having TWO URLs in the same URL :-)

/Jørn

nickdu

unread,
Mar 28, 2015, 10:24:07 PM3/28/15
to api-...@googlegroups.com
I'm confused.

First I see some payload at your root:


    {
    }

What is that?  Doesn't look like hypermedia.  Looks like a simple json object.

You said https://api.hypothetical.com/employees is a link relation, right?  How do you know the URL for that?  I can understand the server would know, but doesn't the URL need to be passed to the client?  I'm used to seeing hypermedia like this:

HAL:

"<link relation>" : {"href" : "<some url>"}

SIREN:

{ rel : ["<link relation>"], "href" : "<some url>"}

Why are you wanting or needing to send link relation URL's for your resources and have the client replace 'api' with 'www'?  Is there something you're gaining by doing that?

Thanks,
Nick

Mark E. Haase

unread,
Apr 3, 2015, 11:57:22 AM4/3/15
to api-...@googlegroups.com
Jørn,

You can simply include the whole API URL in the client URL - like the HAL browser does.

Thanks for the suggestion. This is the clearest solution anybody has provided. I don't think I'll actually do that, but I appreciate the thoughtful answer.


Nick, 

 Doesn't look like hypermedia.

It's a resource that has embedded hyperlinks to other resources.

You said https://api.hypothetical.com/employees is a link relation, right?  How do you know the URL for that? 

That is the URL for that resource.

Why are you wanting or needing to send link relation URL's for your resources and have the client replace 'api' with 'www'?  Is there something you're gaining by doing that?

I know it was a long-winded question, and I don't blame you for not reading the whole thing, but manipulating strings like that is precisely the thing I was seeking to avoid.

--
You received this message because you are subscribed to a topic in the Google Groups "API Craft" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/api-craft/oWjaGZ2ZdHs/unsubscribe.
To unsubscribe from this group and all its topics, send an email to api-craft+...@googlegroups.com.



--
Mark E. Haase
202-815-0201

Jan Schütze

unread,
Apr 4, 2015, 12:12:33 PM4/4/15
to api-...@googlegroups.com
Hi Mark,

in a project for a client, we used a similar approach like the HAL browser (#suffix of the url) because our SPA was 100% static html with an api on a different host.

If however you control the api + www host: why don't you use content negoation instead?

curl -H 'Accept: application/jsob' http://example.org/employee/1

to recieve the JSON and the browser recieves HTML.

If you setup your caching with "Vary: Accept" the clients would not accidently recieve the wrong version. 

That's what we did for other clients and it worked very well.

I have a general question: How does your SPA know how to render the results of your API call? At the moment you only talk about URLs and how to bookmark them. How do you decide on type of response how to render e.g. the Employees list or the Employees Detailpage?

Regards

Jan

You received this message because you are subscribed to the Google Groups "API Craft" group.
To unsubscribe from this group and stop receiving emails from it, send an email to api-craft+...@googlegroups.com.


--
 
  http://dracoblue.net

Mark E. Haase

unread,
Apr 4, 2015, 12:52:32 PM4/4/15
to api-...@googlegroups.com
Hi Jan,

If however you control the api + www host: why don't you use content negoation instead?
curl -H 'Accept: application/jsob' http://example.org/employee/1
to recieve the JSON and the browser recieves HTML.

Thank you for the idea. I did something very much like this a few years ago for another project (it's a public site, but I don't work there anymore: https://scapsync.com). It was sort of an experiment at the time. (I even asked a question on SO before trying it: http://stackoverflow.com/questions/10853207/how-to-use-one-rest-api-for-both-machines-and-humans). I was overall pleased with the approach, but it does have some drawbacks (which are noted on that SO thread).

This would be awkward with an SPA, though, because each of the HTML resources would be [almost?] identical even though the URLs are different.

I have a general question: How does your SPA know how to render the results of your API call? At the moment you only talk about URLs and how to bookmark them. How do you decide on type of response how to render e.g. the Employees list or the Employees Detailpage?

To be concrete: I'm using Angular.dart, which might shed some light on what I'm trying to do.

The SPA performs "routing" based on its own URL. E.g. https://www.example.com/employees invokes the "employees" view. The view itself is responsible for making whatever API call[s] it needs to render itself. So https://www.example.com/employees might request https://api.example.com/employees, but https://www.example.com/dashboard might request https://api.example.com/stats/cpu, https://api.example.com/stats/memory, and https://api.example.com/stats/temperature.

The question I asked is purely theoretical, because I have no problem building something that *works*. I just haven't found an elegant way to build it while still observing the HATEOAS constraint. After reading all of the responses here and thinking on it, I've finally landed here: I'm not going to worry about it. HATEOAS is a property of the service, not the client. I can still build a HATEOAS service even if any particular client chooses not to follow some links, or any links.

Jan Schütze

unread,
Apr 4, 2015, 6:10:41 PM4/4/15
to api-...@googlegroups.com
> This would be awkward with an SPA, though, because each of the HTML resources would be [almost?] identical even though the URLs are different.

If you use angular for this, you can serve one .html exactly the same and include the angular code depending on the request path. Except if you need something, which is indexed by search engines?

> To be concrete: I'm using Angular.dart, which might shed some light on what I'm trying to do.
> The SPA performs "routing" based on its own URL. E.g. https://www.example.com/employees invokes the "employees" view. The view itself is responsible for making whatever API call[s] it needs to render itself. So https://www.example.com/employees might request https://api.example.com/employees, buthttps://www.example.com/dashboard might request https://api.example.com/stats/cpuhttps://api.example.com/stats/memory, andhttps://api.example.com/stats/temperature.

In this case the "dashboard" resource, will have "links" to cpu, memory and temperature, won't it? So this is where HAL's _embedded property fits well:

{
   "_embedded": {
       /* and so on */
   }
}

So your angular-Application can read this links and embed whatever is necessary to render the dashboard. BUT ONLY: If you need this flexibility!


About where HATEOAS happens in your architecture: Actually it depends on the amount of logic you want to have in your client (angular) code or in your service code.

If you want to build new views, without changing the client code: you should have some links/embed-logic in your architecture. Then you build some kind of "browser" for your api, which actually looks like your SPA :). But most of the time this is not want you want to build. It is a really cool technical adventure, but sometimes it's hard to think in such generic way if you want to get it done for a specific use case.

If you build new views (like that dashboard) by composing the api-calls in the client code: having links to fetch should be sufficient. But in this case, also prefixing urls with http://api. or refetch the contents with a different Accept-Header should be fine.

Hope that helps!

--
You received this message because you are subscribed to the Google Groups "API Craft" group.
To unsubscribe from this group and stop receiving emails from it, send an email to api-craft+...@googlegroups.com.
Visit this group at http://groups.google.com/group/api-craft.
For more options, visit https://groups.google.com/d/optout.
Reply all
Reply to author
Forward
0 new messages