Best practices approach for linking and un-linking 2 separate resources

Not applicable

I am building a REST API and need to relate 2 standalone entities together. Having done a full day of Googling I'm no further to having confidence in the correct approach for handling this; therefore I'm seeking advice on the best practice for handling this scenario.

The resources represent an item for sale and the rates of that item. The item can have many associated rates to cater for backdated payments. The application needs to manage the 2 resources separately, but be able to relate the rates to the item once everything has been set up. These entities also need to support unlinking too.

The resources are as follows:

ShopItem
{
    shopItemId: 1,
    title: "sample title",
    rates: https://sample/restapi/shopItem/{id}/rates
}

Rate
{
    rateId: 1,
    title: "title of the rate",
    cost: 100,
    validFrom: "dateTime",
    validTo: "dateTime"
}

The options I have so far would be

1. Use PUT and the body would be a collection of Rate IDs.

This handles both link and unlink.

https://sample/restapi/shopItem/{id}/rates

{  
   [
      {   
         rateId: 1
      },
      {     
         rateId: 2
      }
   ]
}

2. Use POST and the body again would be a collection of Rate IDs.

This handles only link

https://sample/restapi/shopItem/{id}/rates

{  
   [
      {   
         rateId: 1
      },
      {     
         rateId: 2
      }
   ]
}

To handle unlink, a DELETE would be used:

https://sample/restapi/shopItem/{id}/rates

{  
   [
      {   
         rateId: 1
      },
      {     
         rateId: 2
      }
   ]
}

3. Use POST but handle only one resource at a time

This handles only link

https://sample/restapi/shopItem/{id}/rates

{     
   rateId: 1
}

To handle unlink, a DELETE would be used on the shop-item-rate resource. Only can handle one at a time:

https://sample/restapi/shopItemRates

{     
   rateId: 1
}

Hopefully this explains the scenario. Any advice is appreciated.

Solved Solved
0 3 374
1 ACCEPTED SOLUTION

A couple observations:

  • Consider employing IDs and URLs in the API design for linked items.

    GET /item/{itemid}/rates
    	

    returns:

    200 OK
    {
      "rates": [ 
         { 
           "id" : "1",  
           "href" : "/rates/48782" 
         },
         { 
           "id" : "2",  
           "href" : "/rates/3763837" 
         }
      ]
    }	

    or to get one rate:

    GET /item/{itemid}/rates/1
    	

    returns:

    200 OK
    { 
       "id" : "1",  
       "href" : "/rates/48782" 
    }	
  • You can decide if you want your API to resolve the rates when querying the item.

    GET /item/{itemid}/rates
    	

    returns:

    200 OK
    {
      "rates": [ 
         { 
           "id" : "1",  
           "href" : "/rates/48782",
           "rate" : 17.95,
           "etc" : "other rate-specific information here" 
         },
         { 
           "id" : "2",  
           "href" : "/rates/3763837", 
           "rate" : 17.95,
           "etc" : "other rate-specific information here" 
         }
      ]
    }
    	
  • HTTP DELETE does not accept a payload.
    You can use DELETE for your unlink action, but it will be a no-payload request.

    DELETE /item/{itemid}/rates/1
    	

    returns:

    200 OK
    {
       "id" : "2",  
       "href" : "/rates/3763837"
    }
    	

    And the error case:

    DELETE /item/{itemid}/rates/17
    	

    returns:

    404 Not Found
    {
      error: "rate not found", "id": "17" }
    }
    	
  • Whether you accept POST or PUT is a matter of preference and style.
    Some APIs allow POST to mean "create" while PUT is "update". This means they would not be mutually exclusive. Your API could support both. But you can use POST with a query param to denote "adding" a rate.

    POST /item/{itemid}?action=addRate
    {
      "href" : "/rates/96221"
    }
    	

    returns:

    200 OK
    {
      "rates": [ 
         { 
           "id" : "2",
           "href" : "/rates/3763837"
         },
         { 
           "id" : "3",
           "href" : "/rates/96221"
         }
      ]
    }	

    and the error case in which the rate-to-be-added does not exist:

    POST /item/{itemid}?action=addRate
    {
      "href" : "/rates/05461" 
    }
    	

    returns:

    404 Not Found
    {
      "error" : "rate does not exist", "rate" : "/rates/05461" 
    }
    	

    You could also modify the URL to be:

    POST /item/{itemid}/rates?action=add
    

View solution in original post

3 REPLIES 3

A couple observations:

  • Consider employing IDs and URLs in the API design for linked items.

    GET /item/{itemid}/rates
    	

    returns:

    200 OK
    {
      "rates": [ 
         { 
           "id" : "1",  
           "href" : "/rates/48782" 
         },
         { 
           "id" : "2",  
           "href" : "/rates/3763837" 
         }
      ]
    }	

    or to get one rate:

    GET /item/{itemid}/rates/1
    	

    returns:

    200 OK
    { 
       "id" : "1",  
       "href" : "/rates/48782" 
    }	
  • You can decide if you want your API to resolve the rates when querying the item.

    GET /item/{itemid}/rates
    	

    returns:

    200 OK
    {
      "rates": [ 
         { 
           "id" : "1",  
           "href" : "/rates/48782",
           "rate" : 17.95,
           "etc" : "other rate-specific information here" 
         },
         { 
           "id" : "2",  
           "href" : "/rates/3763837", 
           "rate" : 17.95,
           "etc" : "other rate-specific information here" 
         }
      ]
    }
    	
  • HTTP DELETE does not accept a payload.
    You can use DELETE for your unlink action, but it will be a no-payload request.

    DELETE /item/{itemid}/rates/1
    	

    returns:

    200 OK
    {
       "id" : "2",  
       "href" : "/rates/3763837"
    }
    	

    And the error case:

    DELETE /item/{itemid}/rates/17
    	

    returns:

    404 Not Found
    {
      error: "rate not found", "id": "17" }
    }
    	
  • Whether you accept POST or PUT is a matter of preference and style.
    Some APIs allow POST to mean "create" while PUT is "update". This means they would not be mutually exclusive. Your API could support both. But you can use POST with a query param to denote "adding" a rate.

    POST /item/{itemid}?action=addRate
    {
      "href" : "/rates/96221"
    }
    	

    returns:

    200 OK
    {
      "rates": [ 
         { 
           "id" : "2",
           "href" : "/rates/3763837"
         },
         { 
           "id" : "3",
           "href" : "/rates/96221"
         }
      ]
    }	

    and the error case in which the rate-to-be-added does not exist:

    POST /item/{itemid}?action=addRate
    {
      "href" : "/rates/05461" 
    }
    	

    returns:

    404 Not Found
    {
      "error" : "rate does not exist", "rate" : "/rates/05461" 
    }
    	

    You could also modify the URL to be:

    POST /item/{itemid}/rates?action=add
    

Thanks a lot Dino for your in depth answer. Your observations were helpful and point out some other flaws in the API, but was most keen to hear your thoughts on POST & DELETE vs PUT for relating the 2 existing resources together.

I like the idea of using PUT as it is idempotent and I can allow for one endpoint to handle both the linking and un-linking of rates to items.

Not applicable
@Dino-at-Google

gave you good advice about using urls rather than numeric IDs. Your modeling of the problem seems unusual to me. Are rates shared between items? If not, the simplest model would be to embed the rates in the item resources in an array-valued property, and use PATCH to let users maintain the array. json-patch (RFC 6902) defines a PATCH format you can use to let users modify the array. If Rates are sharable, then they do have to be independent resources, but you could still use an array-valued property in the item whose entries are URLs of rates rather than rate objects. The most complex model is the one you chose where there is a separate resource for each (item, rate) pair. This is the equivalent of creating a relational join table to tie items to rates. If you need this complexity, rather than invent hierarchical URL paths, I would model the join resources the same way you would in a database, like this:

{"item": "/item/1234", "rate": "/rate/1234"}

You could create these with the obvious POST to /itemrates or something like that. For GET and DELETE of these you could use urls of the form

/itemrates?item=/item/1234&rate=/rate/1234

I think you should go with the simplest model that fits your scenarios.