21

I am working on an REST API and I am trying to understand how to deal with hierarchical resources.

Background

Let's start with a simple example. In my API I have Users, User profiles and Reviews.

  • Users must have a User Profile associated (a User Profile corresponds to only one User)
  • Users might have a Review associated (a Review corresponds to only one User)

User's resource representation should be:

User: {
   "u1": "u1value", // User's attributes
   "u2": "u2value",
   ...
   "links": [{
       "rel": "profile",
       "href": "http://..." // URI of the profile resource
   }, {
       "rel": "review",
       "href": "http://..." // URI of the review resource
   }]
}

User profile resource representation should be:

UserProfile: {
   "p1": "p1value", // Profile attributes
   "p2": "p2value",
   ...
   "links": [{
       "rel": "owner",
       "href": "http://..." // URI of the user resource
   }]
}

Review resource representation should be:

Review: {
   "r1": "r1value", // Review attributes
   "r2": "r2value",
   ...
   "links": [{
       "rel": "owner",
       "href": "http://..." // URI of the user resource
   }]
}

Resources URIs could be:

  • http://api.example.com/users/{userid}: access to the user resource
  • http://api.example.com/users/{userid}/profile: access to the user's profile resource
  • http://api.example.com/users/{userid}/review: access to the user's review resource

Resource creation: what's the correct way to create a user?

Now I want to create a new user:

  1. POST http://api.example.com/users {"u1": "bar", "u2": "foo"} and I get back the new userid = 42
  2. POST http://api.example.com/users/42/profile {"p1": "baz", "p2": "asd"}
  3. PUT http://api.example.com/users {"u1": "bar", "u2": "foo", links: [{"rel": "profile", "href": "http://api.example.com/users/42/profile"]}

My concerns:

  • What if something breaks between 1 and 2 or 2 and 3?
  • In 3), should the server update automagically the links in the http://api.example.com/users/42/profile, to point to the correct owner?
  • Updating link fields is the proper manner to create relationships? Or should I skip step 3) and let the system guess the relationships according to URI conventions? (I read on several books that URI should be considered as opaque.)
Sachin
  • 40,216
  • 7
  • 90
  • 102
Ameba Spugnosa
  • 1,204
  • 2
  • 11
  • 25
  • what is the reason for having separated objects for user and profile, as they appear to have a mandatory 1 to 1 relation? – njzk2 Apr 08 '13 at 11:28

4 Answers4

17

Your concerns are well-placed and your list of issues is correct. If I may suggest, your approach looks very much like you are using a relational DB approach and are doing an INSERT, retrieving the PK from a sequence which you use for the next INSERT, and so on.

Let the server maintain referential integrity

As an observation, even if following your original scheme, omit step 3 entirely. The URI in links that is visible when you retrieve your user document should be generated by the server based on the existence of the profile record.

For example, if using a relational backend, you SELECT from USERS to get the user record. Next, you SELECT from PROFILES. If there is a record, then you modify the return datastructure to include the reference.

POST entire documents

A common way to resolve the other issues that you bring up is to allow posting of an entire document to the user URL (like NoSQL databases such as MongoDB). Here the document is the user and the profile:

{ 
   "u1": "bar",
   "u2": "foo",
   "profile": {  
                 "p1": "baz",
                 "p2": "asd"
              }
}

In this approach, your end-point on the server receives a nested structure (document) and performs the INSERT into USERS, retrieves the PK, then performs the INSERT into PROFILES using this PK. Doing this on the server side resolves several concerns:

  1. The transaction can be atomic
  2. There is only one network exchange between client and server
  3. The server does the heavy lifting and can validate the entire transaction (eg the user is not created if the profile is not valid)
  4. No need for step 3.

Note that you this approach is in addition to the APIs you have detailed above - you still want to be able to directly access a user's profile.

GET - client can specify fields

It is interesting to compare with APIs from well-established companies. Take LinkedIn for example. In their developer API the default GET for a user returns simply the user's name, headline and URI.

However, if the request specifies additional fields, you can get the nested data, eg the second example in http://developer.linkedin.com/documents/understanding-field-selectors returns the user's name and a list company names for the positions they have held. You could implement a similar scheme for Profiles and Reviews.

PATCH for updating document properties

With inserting and querying out of the way, it might be worth considering how to update (PATCH) data. Overwriting a field is obvious, so you could, for example PATCH to http://api.example.com/users/42 the following:

{ 
   "u1": null,
   "u2": "new-foo",
   "profile": {  "p1": "new-baz"}
}

Which would unset u1, set u2 to new-foo and update the profile's p1 to new-baz. Note that if a field is missing (p2), then the field is not modified. PATCH is preferable to the older PUT as explained in this answer.

If you only need to update the profile, PATCH the new profile record directly to http://api.example.com/users/42/profile

DELETE should cascade

Lastly, deleting can be done with the DELETE method pointing to the resource you want to delete - be it User, Profile or Review. Implement a cascading delete so that deleting a User deletes his/her Profile and Reviews.

Community
  • 1
  • 1
Andrew Alcock
  • 19,401
  • 4
  • 42
  • 60
  • 1
    I would suggest using the `PATCH` method for operations that partially or incrementally modify a resource. `PUT` suggests complete replacement. – Timothy Shields Apr 02 '13 at 02:35
  • @TimothyShields: Thanks - I missed that completely. I'm updating the answer right now. – Andrew Alcock Apr 02 '13 at 03:03
  • good answer. IMO the only thing missing is a form like structure (similar to the "links" in the question examples) that tells the client how and what to POST to create a user. It would make sense to include this in the response to a GET on `http://api.example.com/users/` – Tom Howard Apr 05 '13 at 11:00
  • @TomHoward I believe that what to POST should be explained on API data representation docs. I took the links idea from the HATEOAS paradigm http://en.wikipedia.org/wiki/HATEOAS – Ameba Spugnosa Apr 05 '13 at 19:43
  • 1
    @AmebaSpugnosa. A form is just another hypermedia control. If you put the forms in the API docs, then you just moving those hypermedia controls into an out-of-band document. The beauty of REST (and the HATEOAS constraint), is that this controls are in-band and can change based on the state of the resources. e.g., to update a user, a form would be included in the User's resource representation; however you could have new users in a "locked" state until their email in verified, so the update form for those users would not be present. – Tom Howard Apr 08 '13 at 12:11
2

You should stick to HATEOAS, and dereference the URLs you get on your responses:

For ease of access, lets say User.profile contains the href of the link with rel == profile.

Create the User

With the POST you described... but it should not return an id, but a user, complete with it's links.

User: {
   "u1": "bar", // User's attributes
   "u2": "foo",
   ...
   "profile": "http://api.example.com/users/42/profile",
   "links": [{
       "rel": "profile",
       "href": "http://api.example.com/users/42/profile"
   },
   ...
   ]
}

At this point the profile resource at User.profile (could be http://api.example.com/users/42/profile, or whatever location you migrate to in the future) is be whatever a default profile should be, e.g. an empty document or with just the owner link filled.

Update the Profile

profile = GET User.profile
profile.p1 = "baz"
profile.p2 = "asd"
PUT profile to the same url you just dereferenced

By dereferencing hrefs on your documents instead of constructing urls with id's you get from responses, you client will not have to change when the API changes. Like when - profiles are moved to http://profiles.cdn.example.com/ - profiles get a p3 value

"Old" API clients will keep working without having to change any code.

Chris Wesseling
  • 6,226
  • 2
  • 36
  • 72
1

Actually from successful step (1) you should get HTTP code 201 Created and an address (URL) to the newly created resource, not just the ID number. If step (2) fails your REST API should indicate whether the problem is with the client such as badly formed document (issue code 4xx) or server (5xx). For example if in the meantime resource 42 was deleted, code 404 Not Found should be returned.

Therein lies the problem with stateless REST APIs - they cannot support transactions composed of more than one request. For this to be possible you would have to maintain a session (state) on the server.

By the way, the URL in step (3) in your example suggest that you are substituting all users and probably should read http://api.example.com/users/42.

You have a choice between submitting complete user+profile document at once to be split into two database records in one atomic transaction, or to allow persistence of partial user data i.e. user without a profile.

The choice depends on the context. For example it may be perfectly fine that a user does not have a profile (so it can be provided by the user). Conversely having a profile record, which does not belong to any user is probably not acceptable. Discussion about enforcing this logic goes beyond the scope of your question and will vary by the type of persistent store (database) you choose. Relational databases enforce this using foreign keys.

Victor Olex
  • 1,458
  • 1
  • 13
  • 28
0

I believe your calls should be like this

1) Create a user

POST http://api.example.com/users + params in payload

If it returns HTTP 201 + user id, then you can go on and create the profile. Else you treat the exception the way you want. You should wait for the first call to get back before starting the creation of the profile.

2) Create a profile associated to a user 42 (if the user creation was ok)

POST http://api.example.com/users/42/profile + params in payload

returns HTTP 201 + profile id

Your backend will be responsible for updating your user object and the profile object (and your database) so that users 42 will be linked to the new profile. If the backend cannot link the object you can send back a 500 error explaining what happened.

So in my opinion I would skip step 3.

Now I understand that your point is about the fact that a user MUST have a profile

I see 2 solutions

1) you could create an empty associated profile when creating your user. Then by querying the user you can get the profile id and modify it with a PUT (I really do not like this solution because when you are asking your api to create a user it should only create a user and nothing else but as in fact a profile is mandatory it not as ugly as it seems).

2) you could have a property in you user saying if the user is correct or not (meaning he has an associated profile). At the end of the creation process if your user 42 is not correct you can delete it or retry the profile creation ... Then you could query only correct users with something like /users?isCorrect=true

3) Let the client treat the fact that a user as no profile -> show a popup to ask for creation of the profile ...

Have a look to this document for REST API best practices

Maybe you could also have a look to HAL which tries to deal with relationship between objects.

And last but not least you could follow api-craft google group you might find interesting topics related to your issue there.

Ronan Quillevere
  • 3,699
  • 1
  • 29
  • 44