Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Update/create hierarchical resources REST [closed]

Tags:

rest

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.)
like image 512
Ameba Spugnosa Avatar asked Nov 05 '12 15:11

Ameba Spugnosa


3 Answers

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.

like image 192
Andrew Alcock Avatar answered Nov 04 '22 17:11

Andrew Alcock


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.

like image 39
Chris Wesseling Avatar answered Nov 04 '22 17:11

Chris Wesseling


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.

like image 1
Victor Olex Avatar answered Nov 04 '22 17:11

Victor Olex