Think About Your API Client

Tuesday, January 2, 2018

It’s easy to miscommunicate intentions.

Teams starting to build HTTP based APIs need to think in terms of resources and the operations they want a client to perform on those resources. The resource name should appear in the URL. The operation is defined by the HTTP method. These concepts are important to understand when building APIs for the outside world to consume. Sometimes the frameworks and abstractions we use make it easy to forget the ultimate goal of an HTTP API.

Here's an example.

I came across a bit of code that looks something like the following. The idea is to expose a resource with associated child resources. In my example, the parent is an invoice, the children are line items on the invoice.

public class InvoiceController : Controller
{ 
    [Route("GetAll/{invoiceId}")]
    [HttpGet]
    public IEnumerable<LineItem> GetByInvoiceId(string invoiceId)
    {
        // ... return all items for the invoice
    }

    [Route("Get/{lineItemId}")]
    [HttpGet]
    public LineItem GetByLineItemId(string lineItemId)
    {
       // .. return a specific line item
    }
}

From a C# developers perspective, the class name and method names are reasonable. However, an HTTP client only interacts with the API using HTTP messages. If I am putting together code to send an HTTP GET request to /getall/87, I'm not going to feel comfortable knowing what resource I'm interacting with. The URL (remember the R stands for resource) does not identity the resource in any manner, only an operation (GET, which should be handled by the HTTP message instead of appearing in the URL).

One of the keys to building an effective HTTP API is to map the resources in your domain to a set of URLs. There are various subtleties to take into account, but in general you'll build a more effective API if you think about the interface you are exposing to clients first, and then figure out how to implement the interface. In this scenario, I'd think about each invoice being a resource, and each line item being a nested resource inside a specific invoice. Thinking about the interface first, I'd try to expose an API like so:

GET /invoices/3  <- get details on the invoice with an ID of 3

GET /invoices/3/lineitems  <- get all the line items for an invoice with an id of 3

GET /invoices/3/lineitems/87  <- get the line item with an id of 87 (which is inside the invoice with an id of 3)

There is also nothing wrong with giving a nested resource a top level URL. In other words, a single resource can have multiple locators.  It all depends on how the client will need to use the API.

GET /lineitems/87  <- get the line item with an id of 87

In the end, this interface is more descriptive compared to:

GET getall/87

And the controller could look like the following (not all endpoints listed above are implemented):

[Route("invoices")]
public class InvoiceController : Controller
{
    [Route("{invoiceId}/lineitems")]
    [HttpGet]
    public IEnumerable<LineItem> GetByInvoiceId(string invoiceId)
    {
         // ... return all line items for given invoice
    }

    [Route("{invoiceId}/lineitems/{lineItemId}")]
    [Route("~/lineitems/{lineItemId}"]
    [HttpGet]
    public LineItem GetByLineItemId(string lineItemId)
    {
        // .. return a specific item
    }
}

Just remember contract first. Implementation later.


Comments
gravatar Mikhail Tuesday, January 2, 2018
You have a typo in the last piece of code. Shouldn't be ~/lineitemss
gravatar Scott Tuesday, January 2, 2018
Thank you, will be updated shortly.
gravatar Yadel Lopez Tuesday, January 2, 2018
Great post as usual. Thanks Scott
gravatar Vadim Wednesday, January 3, 2018
I'd like to guess a bit. In real world this two routes will have different behaviour: 1. "~/lineitems/{lineItemsId}" - this will return lineitem 2. "{invoiceId}/lineitems/{lineItemId}" - this will check if LineItem belongs to the Invoice and if and only if it's true will return Lineitem.
gravatar Scott Wednesday, January 3, 2018
@Vadim: Yes, that's possible (assuming every line item has a unique ID, I guess there could be systems where line item has a unique ID within the scope of an invoice, in which case we'd probably only have the one route with an invoice ID).
gravatar CS Wednesday, January 3, 2018
Good post. As an 'enterprise developer' trying to communicate to our rpc / wsdl developers the importance of resource design and forethought, this can't be stressed enough. There are some great, albeit older, books on the subject.. Leonard Richardson and Sam Ruby did a decent job back in 2007 with 'RESTful Web Services' though some of the guidelines are a bit outdated in the modern API world. Developers I've interacted with often get hung up on your last point, re: having multiple 'routes' to a destination. In the end this is the design of the web, as an interconnected resource warehouse.
gravatar DM Thursday, January 4, 2018
The first route should be "{invoiceId}/lineitems" and in the second route lineItemId instead of lineItemsId
gravatar Scott Thursday, January 4, 2018
Good catch. Copy, paste, and not updated error.
gravatar Wyatt Barnett Thursday, January 4, 2018
This is why I always write "client 0" when building apis -- there is nothing like using something in anger to figure out the design flaws.
gravatar MV10 Saturday, January 6, 2018
"The URL (remember the R stands for resource) does not identify the resource in any manner" ... because that's what URIs are for. Taken out of context, I know, but getting URL vs URI wrong in REST articles is kind of a pet peeve, REST is where the difference matters the most. Great points in the article, though, I see this problem all the time in production code. In my own REST work I've always tried to ensure that if I bookmark a GET, I'll understand what it's doing years later just by checking the URI.
gravatar green bongo Saturday, January 6, 2018
If this is a Invoice Controller , why this is returning only Line Item ? Disgusting design. Entities .. table invoice : InvoiceId -key table lindeitemid :(InvoiceId,lineItemID) -key Is there is any reason for this ? "~/lineitems/{lineItemId}" This return some random stuff from db ?
gravatar Scott Saturday, January 6, 2018
@MV10 - more of a precursor to a REST discussion. @green bongo - The Invoice can manage the line items inside, and the post says "not all endpoints listed above are implemented", but thanks for skimming the article quick enough as to miss the point.
Your Comment