REST Complex/Composite/Nested Resources [closed]
Asked Answered
K

2

192

I'm trying to wrap my head around the best way to address concepts in a REST based API. Flat resources that don't contain other resources are no problem. Where I'm running into trouble are the complex resources.

For instance, I have a resource for a comic book. ComicBook has all sorts of properties on it like author, issue number, date, etc.

A comic book also has a list of 1..n covers. These covers are complex objects. They contain a lot of information about the cover: the artist, a date, and even a base 64 encoded image of the cover.

For a GET on ComicBook I could just return the comic, and all of the covers including their base64'ed images. That's probably not a big deal for getting a single comic. But suppose I am building a client app that wants to list all of the comics in the system in a table.
The table will contain a few properties from the ComicBook resource, but we're certainly not going to want to display all the covers in the table. Returning 1000 comic books, each with multiple covers would result in a ridiculously large amount of data coming across the wire, data that isn't necessary to the end user in that case.

My instinct is to make Cover a resource and have ComicBook contain covers. So now Cover is a URI. GET on comic book works now, instead of the huge Cover resource we send back a URI for each cover and clients can retrieve the Cover resources as they require them.

Now I have a problem with creating new comics. Surely I'm going to want to create at least one cover when I create a Comic, in fact that's probably a business rule.
So now I'm stuck, I either force the clients to enforce business rules by first submitting a Cover, getting the URI for that cover, then POSTing a ComicBook with that URI in the list, or my POST on ComicBook takes in a different looking resource than it spits out. The incoming resources for POST and GET are deep copies, where the outgoing GETs contain references to dependent resources.

The Cover resource is probably necessary in any case because I'm sure as a client I'd want to address covers direction in some cases. So the problem exists in a general form regardless of the size of the dependent resource. In general how do you handle complex resources without forcing the client to just "know" how those resources are composed?

Kunkel answered 18/8, 2011 at 8:37 Comment(3)
does using RESTFUL SERVICE DISCOVERY make sense?Concubine
I'm trying to adhere to HATEAOS which, to my mind, runs counter to using something like that but I'll take a look.Kunkel
Different question in the same spirit. However the ownership is different to your proposed solution (The one in the question). #20951919Shawnee
B
70

@ray, excellent discussion

@jgerman, don't forget that just because it's REST, doesn't mean resources have to be set in stone from POST.

What you choose to include in any given representation of a resource is up to you.

Your case of the the covers referenced separately is merely the creation of a parent resource (comic book) whose child resources (covers) may be cross-referenced. For example, you might also wish to provide references to authors, publishers , characters, or categories separately. You may wish to create these resources separately or before the comic book which references them as child resources. Alternatively, you may wish to create new child resources upon creation of the parent resource.

Your specific case of the covers is slightly more complex in that a cover really does require a comic book, and visa versa.

However, if you consider an email message as a resource, and the from address as a child resource, you can obviously still reference the from address separately. For example, get all from addresses. Or, create a new message with a previous from address. If email was REST, you could easily see that many cross-referenced resources could be available: /received-messages, /draft-messages, /from-addresses, /to-addresses, /addresses, /subjects, /attachments, /folders, /tags, /categories, /labels, et al.

This tutorial provides a great example of cross-referenced resources. http://www.peej.co.uk/articles/restfully-delicious.html

This is the most common pattern for automatically-generated data. For example, you don't post a URI, ID, or creation date for the new resource, as these are generated by the server. And yet, you can retrieve the URI, ID, or creation date when you get the new resource back.

An example in your case of binary data. For example, you want to post binary data as child resources. When you get the parent resource you can represent those child resources as the same binary data, or as URIs which represent the binary data.

Forms & parameters are already different than the HTML representations of the resources. Posting a binary/file parameter which results in a URL isn't a stretch.

When you get the form for a new resource (/comic-books/new), or get the form to edit a resource (/comic-books/0/edit), you are asking for a forms-specific representation of the resource. If you post it to the resource collection with content-type "application/x-www-form-urlencoded" or "multipart/form-data", you are asking the server to save that type representation. The server can respond with the HTML representation which was saved, or whatever.

You may want to also allow for an HTML, XML, or JSON represention to be posted to the resource collection, for purposes of an API or similar.

It is also possible to represent your resources and workflow as you describe, taking into account covers posted after the comic book, but requiring comic books to have a cover. Example as follows.

  • Allows delayed cover creation
  • Allows comic book creation with required cover
  • Allows covers to be cross-referenced
  • Allows multiple covers
  • Create draft comic book
  • Create draft comic book covers
  • Publish draft comic book

GET /comic-books
=> 200 OK, Get all comic books.

GET /comic-books/0
=> 200 OK, Get comic book (id: 0) with covers (/covers/1, /covers/2).

GET /comic-books/0/covers
=> 200 OK, Get covers for comic book (id: 0).

GET /covers
=> 200 OK, Get all covers.

GET /covers/1
=> 200 OK, Get cover (id: 1) with comic book (/comic-books/0).

GET /comic-books/new
=> 200 OK, Get form to create comic book (form: POST /draft-comic-books).

POST /draft-comic-books
title=foo
author=boo
publisher=goo
published=2011-01-01
=> 302 Found, Location: /draft-comic-books/3, Redirect to draft comic book (id: 3) with covers (binary).

GET /draft-comic-books/3
=> 200 OK, Get draft comic book (id: 3) with covers.

GET /draft-comic-books/3/covers
=> 200 OK, Get covers for draft comic book (/draft-comic-book/3).

GET /draft-comic-books/3/covers/new
=> 200 OK, Get form to create cover for draft comic book (/draft-comic-book/3) (form: POST /draft-comic-books/3/covers).

POST /draft-comic-books/3/covers
cover_type=front
cover_data=(binary)
=> 302 Found, Location: /draft-comic-books/3/covers, Redirect to new cover for draft comic book (/draft-comic-book/3/covers/1).

GET /draft-comic-books/3/publish
=> 200 OK, Get form to publish draft comic book (id: 3) (form: POST /published-comic-books).

POST /published-comic-books
title=foo
author=boo
publisher=goo
published=2011-01-01
cover_type=front
cover_data=(binary)
=> 302 Found, Location: /comic-books/3, Redirect to published comic book (id: 3) with covers.

Busby answered 21/9, 2011 at 13:18 Comment(12)
I'm a total newbie to this, and trying to learn it in a hurry. I found this extremely helpful. However, in the other blogs etc. I've been reading today, the use of GET to perform an operation (particularly an operation that is not idempotent) would be frowned on. So shouldn't it be POST /draft-comic-books/3/publish ?Induction
@GaryMcGill In his example, /draft-comic-books/3/publish only returns an HTML form (doesn't modify any data).Foppery
@Olivier is correct. The word publish is there to denote what the form does. However, because you want to keep the verbs confined to HTTP methods, you should be posting to a resource for published comic books. ... If this were a website, you might need a URI for the form to publish something. ... Although, if the publish action was merely a single button on the comic book page, that single-button form could post directly to the /published-comic-books URI.Busby
@Alex, in the POST request I would instead return a 201 Created, with the URL of the new resource as Location in the response headers.Ibo
Would it be possible to do this: posts/1/comments/2. Is that 2 the absolute ID of the comment, such that comments/2 would retrieve the exact same resource? I'm guessing no, because child resources ids should be uniquely associated with the parent resource. Therefore if a resource is accessible via posts/1/comments/2, this means is something unique to the comments of post 1, it should not be available under comments/2 because posts/2/comments/2 would simultaneously be potentially possible. Therefore for the use case of comments, I think only posts/X/comments is a useful URL.Lipase
Usually you want to always use unique IDs. So /posts/1/comments/2 (nested) is the same as /comments/2 (non-nested). Usually routes prefer the non-nested format. That is, /posts/2/comments/2 is invalid because comment ID 2 does not go with post ID 2. As you said, the child list is required to be linked from the parent /posts/1/comments.Busby
@Busby In the POST /draft-comic-books/3/covers I wonder if your controller method has a comic book parameter or a comic book id parameter. I'm doing my first REST app and I went for the former with the object (and not only its long id) being passed in to the controller method, when inserting a child object. Any design tip is very welcome.Satire
@Stephane, usually, since a covers is a nested resource, the parent resource comic-book ID 3 would already exist. For instance, when creating a new draft, you would issue the following requests. 1. POST /draft-comic-books (send new comic book data) to create the comic book. 2. That redirects to GET /draft-comic-books/3 to view the book just created. Then POST /draft-comic-books/3/covers (send new cover data) to create a cover. That redirects to GET /draft-comic-books/3/covers/1 to view the cover just created.Busby
@Busby The redirects are done on the server side I suppose. My POST controllers don't do such a redirect. They simply return the created resource in the response. Do you also do a redirect for a PUT ?Satire
@Stephane, you can do a redirect for a PUT if you are following the PRG (post-redirect-get) pattern. PUT is usually for updating a resource, not creating it.Busby
@Busby You'd go for a redirect in POST and PUT then. I wonder if I should modify my few controllers and do the redirect instead of returning the created resource. The newbie facing two alternatives that work both...Satire
@Stephane, redirects just make everything simpler for the controllers. Even for an API, it's simpler having the create controller return the location for the new content, and then letting the show controller handle the display of the new content. Although, it's nicer/simpler for the client of the API to just get the content and not bother with redirects.Busby
S
51

Treating covers as resources is definitely in the spirit of REST, particularly HATEOAS. So yes, a GET request to http://example.com/comic-books/1 would give you a representation of book 1, with properties including a set of URIs for covers. So far so good.

Your question is how to deal with comic book creation. If your business rule was that a book would have 0 or more covers, then you have no problem:

POST http://example.com/comic-books

with coverless comic book data will create a new comic book and return the server generated id (lets say it comes back as 8), and now you can add covers to it like so:

POST http://example.com/comic-books/8/covers

with the cover in the entity body.

Now you have a good question which is what happens if your business rule says there always must be at least one cover. Here are some choices, the first of which you identified in your question:

  1. Force the creation of a cover first, now essentially making cover a non-dependent resource, or you place the initial cover in the entity body of the POST that creates the comic book. This as you say means that the representation you POST to create will differ from the representation you GET.

  2. Define the notion of a primary, or initial, or preferred, or otherwise-designated cover. This is likely a modeling hack, and if you did that it would be like tweaking your object model (your conceptual or business model) in order to fit a technology. Not a great idea.

You should weigh these two choices against simply allowing coverless comics.

Which of the three choices should you take? Not knowing too much about your situation, but answer the general 1..N dependent resource question, I would say:

  • If you can go with 0..N for your RESTful service layer, great. Perhaps a layer between your RESTful SOA can handle the further business constraint if at least one is required. (Not sure how that would look but it might be worth exploring.... end users don't usually see the SOA anyway.)

  • If you simply must model a 1..N constraint, then ask yourself whether covers might just be sharable resources, in other words, they might exist on things other than comic books. Now they are not dependent resources and you can create them first and supply URIs in your POST that creates comic books.

  • If you need 1..N and covers remain dependent, simply relax your instinct to keep the representations in POST and GET the same, or make them the same.

The last item is explained like so:

<comic-book>
  <name>...</name>
  <edition>...</edition>
  <cover-image>...BASE64...</cover-image>
  <cover-image>...BASE64...</cover-image>
  <cover>...URI...</cover>
  <cover>...URI...</cover>
</comic-book>

When you POST you allow existing uris if you have them (borrowed from other books) but also put in one or more initial images. If you are creating a book and your entity has no initial cover-image, return a 409 or similar response. On GET you can return URIs..

So basically you're allowing the POST and GET representations to "be the same" but you just choose not to "use" cover-image on GET nor cover on POST. Hope that makes sense.

Salop answered 18/8, 2011 at 9:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.