One of the fundamental constraints of HTTP and the central design feature of REST is a uniform interface provided by (among other things) a small, fixed set of methods that apply universally to all resources. The uniform interface constraint has a number of upsides and downsides. I'm quoting from Fielding liberally here.
A uniform interface:
- is simpler.
- decouples implementations from the services that they provide.
- allows a layered architecture, including things like HTTP load balancers (nginx) and caches (varnish).
On the other hand, a uniform interface:
- degrades efficiency, because information is transferred in a standardized form rather than one which is specific to an application's needs.
The tradeoffs are "designed for the common case of the Web" and have allowed a large ecosystem to be built which provides solutions to many of the common problems in web architectures. Adhering to a uniform interface will allow your system to benefit from this ecosystem while breaking it will make it that difficult. You might want to use a load balancer like nginx but now you can only use a load balancer that understands DRAFT and CURATE. You might want to use an HTTP cache layer like Varnish but now you can only use an HTTP cache layer that understands DRAFT and CURATE. You might want to ask someone for help troubleshooting a server failure but no one else knows the semantics for a CURATE request. It may be difficult to change your preferred client or server libraries to understand and correctly implement the new methods. And so on.
The correct* way to represent this is as a state transformation on the resource (or related resources). You don't DRAFT a post, you transform its draft
state to true
or you create a draft
resource that contains the changes and links to previous draft versions. You don't CURATE a post, you transform its curated
state to true
or create a curation
resource that links the post with the user that curated it.
* Correct in that it most closely follows the REST architectural principles.
Where we should put search action?
In GET /search/:text
. This will return a JSON array containing the matches, every match containing the album it belongs to. This makes sense, because the client may be interested not in the track itself, but the entire album (imagine that you are searching for a song which, you believe, was in the same album as the one you remember the name).
it will be not that good to return parent ids with each so. Am I wrong?
Individual tracks can contain the album. This will ensure that the track representation is uniform if you can get a track either through an album or through search (no album here).
Which is better?
As previously stated, including the album makes sense. While the third point (with the relative URI) can be interesting in some cases (you don't have to think about the way the URI should be formed), it has a drawback of not providing explicitly the album. The fourth point corrects this. If you see the benefit of having the relative URI in the response, you can combine the point 3 and 4.
Or maybe I'm dumb?
Choosing good URIs is not an easy task, especially since there is no single right answer. If you develop the client at the same time as the API, it may help you to visualize better how the API could be used. This being said, other people may then prefer other usages you weren't thinking about when developing the API.
An aspect which may be problematic is how you organize data internally, i.e. the usage of a hierarchy. From your comment, you are wondering what should contain a response to GET /artist/1/album/10/song/3/comment/23
, which shows a very tree-oriented vision. This can lead to a few issues when extending the system later. For instance:
- What if a song doesn't have an album?
- What if an album has several artists?
- What if you want to add a feature which makes it possible to comment albums?
- What if there should be comments of comments?
- etc.
This is essentially the problem I explained in my blog: a tree representation has too many limitations to be effectively used in many cases.
What happens if you destroy the hierarchy? Let's see.
GET /albums/:albumId
returns a JSON containing the meta information about the album (such as the year when it was published or the URI of the JPEG showing the album cover) and an array of tracks. For example:
GET /albums/151
{
"id": 151,
"gid": "dbd3cec7-b927-423f-894b-742c4c7b54ce",
"name": "Yellow Submarine",
"year": 1969,
"genre": "Psychedelic rock",
"artists": ["John Lennon", "Paul McCartney", ...],
"tracks": [
{
"id": 90224,
"title": "Yellow Submarine",
"length": "2:40"
},
{
"id": 83192,
"title": "Only a Northern Song",
"length": "3:24"
}
...
]
}
Why do I include, for instance, the length of each track? Because I imagine that the client showing an album may be interested by listing the tracks by title, but also show the length of each track—most clients do. On the other hand, I may not show the composer(s) or the artist(s) for every track, because I decide that this information is not necessary at this level. Obviously, your choices may be different.
GET /tracks/:trackId
returns the information about a specific track. Since there is no hierarchy any longer, you don't need to guess the album or the artist: the only thing you really have to know is the identifier of the track itself.
Or maybe even not? What if you can specify it by name with GET /tracks/:trackName
?
GET /tracks/Only%20a%20Northern%20Song
{
"id": 83192,
"gid": "8d9c4311-9d7b-40a4-8aeb-4fe96247fe2b",
"title": "Only a Northern Song",
"writers": ["George Harrison"],
"artists": ["John Lennon", "Paul McCartney", "Ringo Starr"],
"length": "3:24",
"record-date": 1967,
"albums": [151, 164],
"soundtrack": {
"uri": "http://audio.example.com/tracks/static/83192.mp3",
"alias": "Beatles - Only a Northern Song.mp3",
"length-bytes": 3524667,
"allow-streaming": true,
"allow-download": false
}
}
Now look closer at albums
; what do you see? Right, not one, but two albums. If you have an hierarchy, you can't do that (unless you duplicate the record).
GET /comments/:objectGid
. You may have spotted the ugly GUIDs in the responses. Those GUIDs make it possible to identify the entity across the database in order to perform tasks which can be applied to albums, or artists, or tracks. Such as commenting.
GET /comments/8d9c4311-9d7b-40a4-8aeb-4fe96247fe2b
[
{
"author": {
"id": 509931,
"display-name": "Arseni Mourzenko"
},
"text": "What a great song! (And I'm proud of the usefulness of my comment)",
"concerned-object": "/tracks/83192"
}
]
The comment references the concerned object, making it possible to go to it when accessing the comment outside its context (for instance when moderating the latest comments through GET /comments/latest
).
Note that this doesn't mean that you should avoid any form of hierarchy in your API. There are cases where it makes sense. As a rule of thumb:
If the resource makes no sense outside the context of its parent resource, use hierarchy.
If the resource can live (1) alone or (2) in a context of parent resources of different types or (3) have multiple parents, the hierarchy should not be used.
For instance, lines of a file make no sense outside the context of a file, so:
GET /file/:fileId
and:
GET /file/:fileId/line/:lineIndex
are fine.
Best Answer
REST API over HTTP 1.1
These are (summarising) the "principles" we should considerate if we want to be RESTful-compliant. Among many other things (see also CoRE RFC6690)
Path variables
These variables are used for referencing resources within a hierarchy. (independently of the Method)
Query variables
These variables are used as modifiers of the response. By modifiers we mean: filters, pagination, attribute selection,etc.
Response modification:
AppId is still working for referencing. It gives the scope/context where to look at (users of the app X). Changing this value we are not modifying the response, we are switching the resource.
These variables could be used with PUT, POST, DELETE in order to perform these actions over a subset of resource, but in the words of the OP
(And I agreed)
Body variables
Being dogmatic, we use POST to create new resources, and PUT to modify an existing one.
Both of them require a request body, where the resource to create/update is represented in its final status.
Note: Here is important to make reference to their idempotent nature
Again, appId and userId are mere referencing variables.
Summarizing
Here the specifications: RFC7230 - RFC7235
SOA Webservices over HTTP 1.1
I have separated SOA WS from API RESTful because these are 2 different designs where each of them uses the mentioned Http variables in a different way.
These WS does not use URI for web resource referencing. They use them to expose procedures.
In this approaches, GET and POST are predominant.
Path and body variables are used as procedure arguments.
Query variables keep its function of response modificator.
The specs to implement depends on the needs of the system. As @Eric has pointed out, there are specs like RPC that can be applied to this sort of Webservices.