19

Okay, we have two resources: Album and Song. Here is API:

GET,POST /albums
GET,POST /albums/:albumId
GET,POST /albums/:albumId/songs
GET,POST /albums/:albumId/songs/:songId

We know that we hate some song, it is called Susy, for example. Where we should put search action?

Another question. Okay, now it is a it more real. We open album 1 and load all songs. We create JS objects, each holds song data and has few methods like so: remove, update.

Song object has ID, name and stuff, but has no clue about which parent it belongs to, because we retrieve list of songs by query and it will be not that good to return parent ids with each so. Am I wrong?

So, I see few solutions, but I'm not sure really.

  1. Make parent id optional - as get-parameter. This approach I use currently, but I feel it is ugly one.

    List,Create /songs?album=albumId
    Update,Delete /songs/:songId
    Get /songs/?name=susy # also, solution for first question
    
  2. Hybrid. It is now that handy since we need album id to perform OPTIONS query to get meta-data.

    List,Create /album/:albumId/songs
    Update,Delete /songs/:songId
    POST /songs/search # also, solution for first question
    
  3. Return full url with each resource instance. API is same, but we will get songs like this:

    id: 5
    name: 'Elegy'
    url: /albums/2/songs/5
    

    I heard this approach is called HATEOAS.

  4. So... To provide parent ID

    id: 5
    name: 'Elegy'
    albumId: 2
    

Which is better? Or maybe I'm dumb? Throw some advices, guys!

29

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.

  1. 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.

  2. 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).

  3. 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.

  • Ye, from search, I can return full album info too, it will make another resource - SongSearchResult, it is fine, I assume. But what is about URLs? Should I provide parentID with each object and use it as GET-parameter or normal part of the url? What if I have depth>2? /artist/1/album/10/song/3/comment/23 - it is insane to provide every id of artist, album and song in comment object, but I heard it is a way to go, but isn't is dsigusting?! – dt0xff Mar 2 '15 at 20:28
  • @dt0xff: I edited my answer. I think it should now give you a clear image of how the depth can be avoided. – Arseni Mourzenko Mar 2 '15 at 21:11
  • Yea, it is clear now that it is much easier to implement entry-point for each resource(except some like line or other functional thing) w/o adding it to parent by url. Thank you, you convinced me that my choice was right and "common approach"(really, many nested things.. restangular is built on it) isn't that good. – dt0xff Mar 3 '15 at 14:23
  • Great answer. I have a few objections though. "What if an album has several artists?" Different URIs can identify the same resource since the binary relation URI -> resource is right-unique (many-to-one). So the URIs /artists/foo/albums/qux and /artists/bar/albums/qux can perfectly identify the same album resource. In other words, the path component in a URI represents a graph hierarchy, not necessarily a tree hierarchy, which makes it suitable for representing not only categories but also tags. – Maggyero May 2 at 9:47
  • … "This is essentially the problem I explained in my blog: a tree representation has too many limitations to be effectively used in many cases." So no, this is not a problem. "What if you want to add a feature which makes it possible to comment albums?" That's not a problem either: /artists/foo/albums/qux/comments/7. "What if there should be comments of comments?" Likewise: /artists/foo/albums/qux/song/5/comments/2/comments/8. – Maggyero May 2 at 10:01

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.