Search code examples
apirestarchitectureuniform-interface

Avoiding building (REST API) URLs on the frontend


I've been working on backends for a while now and only recently started working a bit on the frontend, which got me nearer to an end-to-end REST implementation.

More to the point, an important principle of REST is to make it discoverable and consistent, so that the client will know how to deal with resources universally (HATEOAS, JsonApi etc). I've been reading this Google article and there's the following point:

If an API uses HTTP simply and directly, it will only have to document three or four things. (And if an API requires you to read a lot of documentation to learn how to use it, then it is probably not using HTTP as the uniform API.) The four elements of an HTTP API are:

  1. A limited number of fixed, well-known URLs. These are analogous to the names of the tables in a database. For optional extra credit, make all the fixed URLs discoverable from a single one.

and later on....

There is also a shortage of people who understand how to design good HTTP/REST APIs. Unfortunately, we see many examples of APIs that attempt to adopt the entity-oriented HTTP/REST style, but fail to realize all the benefits because they do not follow the model consistently. Some common mistakes are:

  1. Using "local identifiers" rather than URLs to encode references between entities. If an API requires a client to substitute a variable in a URI template to form the URL of a resource, it has already lost an important part of the value of HTTP’s uniform interface. Constructing URLs that encode queries is the only common use for URI templates that is compatible with the idea of HTTP as a uniform interface.

I agree with both, but I fail to see how this can be achieved.

So here's the scenario:

  • API endpoints:
    • GET openapi.json / wadl / whatever-discovery-mechanism
      /articles/
      /articles/$id - only for Option 2 below
      ... (maybe for each entity operation in case of exhaustive discovery like openapi,
           but I'd want to keep it minimal for now)
      
    • GET /articles
      {
          data: [
              {
                  title: "Article 1",
                  links: {
                      self: "/articles/1",
                      ...
                  }
              }
          ]
      }
      
    • GET /articles/$id
    • DELETE /articles/$id
    • ...
  • Frontend URLs:
    • GET /site/articles - a page showing a list / table of articles
    • GET /site/articles/1 - a page with a form for editing that article

When navigating to /site/articles, the frontend would know to call /articles API endpoint - which is one of the "limited fixed urls" Google mentions. Deleting / updating is also done given the links returned with the article entity. With client-side navigation, the frontend can also "redirect" to /site/articles/1.

The tricky part is when a user navigates directly to /site/articles/1 - how would the page know to call /articles/$id without building the URL itself (or somehow translating it)?

These are the options I see:

  1. building the URL (this is basically "common mistake no.1" mentioned above)
    // /site/articles/1
    const apiUrl = '/articles/' + location.pathname.split('/')[3]
    // /articles/1
    
  2. building the URL from discovery links (a variation of previous option, still quite as bad IMO)
    // /site/articles/1
    const apiUrl = api.endpoints.articles + location.pathname.split('/')[3] // or replace or whatever
    // /articles/1
    
  3. encoding the entity "self" link in the frontend URL
    // /site/articles/L2FydGljbGVzLzE=
    const apiUrl = atob(location.pathname.split('/')[3])
    // /articles/1
    
    My concern with this is:
    • it's kinda ugly
    • insecure (xsrf / open redirect ...)
    • it forces the frontend to build URLs anyway (that only it understands)
  4. encoding the entity identifier (which, as I take, is the "self" link) and then looking it up in /articles to then call the returned link
    // /site/articles/L2FydGljbGVzLzE=
    const entityId = atob(location.pathname.split('/')[3])
    const apiUrl = api.get(api.endpoints.articles)
        .first(article => article.links.self === entityId)
        .links.self
    // /articles/1
    
    • even uglier than 3. 😅
    • safe enough
    • seems pointless at first glance...

Solution

  • If you are concerned that a user navigates directly to a page by typing in a URL, then that IS one of the fixed well-known URLs. It's likely anything that is "bookmark-able" will be in that list.

    the key here is the phrase "encode references between entities". This isn't a link between entities, it is an initial entry-point, and as such it is ok to build up the URL from scratch. This is an inflexibility, but as an entry-point you have little choice. The "common mistake" would be embedding that "URL-building" throughout the application as you navigate relationships. I.e. having a list of "commenters" on an article by userid, and building those URLs up by coupling the path to users in the articles page.