Modifying one-to-many relationships with HATEOAS

609 views
Skip to first unread message

Matthew Richardson

unread,
Jul 21, 2015, 9:14:50 AM7/21/15
to api-...@googlegroups.com
Hi,

I'm a relative beginner to the world of API design, and am currently working my way through RESTful concepts, and starting to get my head around HATEOAS.

I mostly understand what is going on, and can envisage how to generate and use links for many scenarios... apart from one, which I can't quite wrap my head around.

Supposing we have a collection of library books, each of which has a current borrower.

The database has a 'book' table, which contains a relationship to the 'borrower' table, which can be easily transformed into a HATEOAS link, such that the data might look something like:

{
   
"book": "APIs are Cool",
   
"author": "John Smith",
   
"links": [
       
{
           
"rel": "borrower",
           
"href": "http://api.example.com/borrowers/2"
       
}
   
]
}

It's easy to see how the client can consume this data and 'look up' borrower '2' as necessary.

However, suppose the client now wants to change the borrower.  The client can (relatively easily) look up the new borrower by stripping the id from the link href and get all borrowers, but it's not clear what to put in a POST/PUT back to the server to indicate this change.

A few options I have envisaged are:

1) Post back the HATEOAS data intact, with the href modified to point to the new borrower - the server resolves this back to the borrower_id to update the db relationship.

This feels a bit dirty - the client shouldn't really be sending HATEOAS-style data to the server... should it?


2) Have the server add a 'data' field to the links section that contains the id - something like:

  "links": [
       
{
           
"rel": "borrower",
           
"href": "http://api.example.com/borrowers/"
           
"id": 2
       
}
   
]

Again the client posts back the HATEOAS data, and the server knows to only consider the 'id' field.


3) Have the links data reference a field in the 'real' data that the client should modify:

{
   
"book": "APIs are Cool",
   
"author": "John Smith",
   
"borrower_id": 2
   
"links": [
       
{
           
"rel": "borrower",
           
"href": "http://api.example.com/borrowers/2",
           
"field": "borrower_id"
       
}
   
]
}

Here the client can discard the HATEOAS data, and only 'real' fields are POSTed to the server. This feels most 'correct' somehow, but is also a bit of a duplication of data.

2 and 3 feel nicer than 1 - but neither of them really feels 'correct'.  I could of course create a completely separate 'relationships' API call, and maintain another table of relationships between borrowers and books, but this seems slightly artificial for one-to-many relationships - and would make 'must have a relationship' constraints difficult to implement, due to the chicken-egg problem of item versus relationship creation order.

I'm probably missing something obvious here, hence this post!  If anyone can offer any comments on their preferred approach in this situation I'd be very grateful.  No doubt there is a better approach out there I haven't yet seen...

Cheers,

Matthew

Jørn Wildt

unread,
Jul 21, 2015, 10:03:49 AM7/21/15
to api-...@googlegroups.com
Here's my few cents of input ...

This feels a bit dirty - the client shouldn't really be sending HATEOAS-style data to the server... should it?

It can certainly send back URLs - nothing wrong in that. Just be aware that URLs can point to resources which are not what you expect. In this case you expect the client to include a link to a person (the borrower) - but what if it was a link to a book? Or a link to something outside of your API? Maybe even just http://slashdot.org :-) The upside of URLs is that it allows you to integrate data from different APIs - the borrower could be a "person" on a completely separate system ... but that requires a bit of standardization between the two systems and its probably overkill for your example.

I would recommend sending back identifiers of entities/persons/borrowers in your own API for a beginning. But I would say that the really "RESTful" distributed solution would be to use URLs as identifiers - but it does complicate matters a bit.

So how do we tell the client about this? I would include separate links (or rather actions/links/forms as it will be more than simply links) for the various business operations available on a book. In the Mason hypermedia format it would look like this with one hypermedia "control" for each of the available links/actions on the book:

{
  "book": "APIs are Cool",
  "author": "John Smith",
  "@controls":
  {
    "borrower":
    {
      "title": "Link to current borrower of the book",
    },
    "return":
    {
      "title": "Return book to library",
      "method": "POST"
    },
    "borrow":
    {
      "title": "Borrow book",
      "method": "POST",
      "schema": { ... JSON schema describing what to include in the POST data - in this case the borrower ID or URL ... }
    },
  }
}

Actually you should use URLs as link relations (control names in Mason), so it could be:

{
  "book": "APIs are Cool",
  "author": "John Smith",
  "@controls":
  {
    {
      "title": "Link to current borrower of the book",
    },
    {
      "title": "Return book to library",
      "method": "POST"
    },
    {
      "title": "Borrow book",
      "method": "POST",
      "schema": { ... JSON schema describing what to include in the POST data - in this case the borrower ID or URL ... }
    },
  }
}

The two controls "return" and "borrow" would never be available at the same time since you can only return a borrowed book and vice versa.

For an introdcution to Mason see https://github.com/JornWildt/Mason

/Jørn




--
You received this message because you are subscribed to the Google Groups "API Craft" group.
To unsubscribe from this group and stop receiving emails from it, send an email to api-craft+...@googlegroups.com.
Visit this group at http://groups.google.com/group/api-craft.
For more options, visit https://groups.google.com/d/optout.

Reply all
Reply to author
Forward
0 new messages