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:
- 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:
- 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:
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
GET /site/articles
- a page showing a list / table of articlesGET /site/articles/1
- a page with a form for editing that articleWhen 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:
// /site/articles/1
const apiUrl = '/articles/' + location.pathname.split('/')[3]
// /articles/1
// /site/articles/1
const apiUrl = api.endpoints.articles + location.pathname.split('/')[3] // or replace or whatever
// /articles/1
// /site/articles/L2FydGljbGVzLzE=
const apiUrl = atob(location.pathname.split('/')[3])
// /articles/1
My concern with this is:
/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
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.