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.