I am working on an REST API
and I am trying to understand how to deal with hierarchical resources.
Let's start with a simple example. In my API I have Users, User profiles and Reviews.
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 resourcehttp://api.example.com/users/{userid}/profile
: access to the user's profile resourcehttp://api.example.com/users/{userid}/review
: access to the user's review resourceNow I want to create a new user:
POST http://api.example.com/users {"u1": "bar", "u2": "foo"}
and I get back the new userid = 42POST http://api.example.com/users/42/profile {"p1": "baz", "p2": "asd"}
PUT http://api.example.com/users {"u1": "bar", "u2": "foo", links: [{"rel": "profile", "href": "http://api.example.com/users/42/profile"]}
My concerns:
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:
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.
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
.
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With