Named Endpoints in RESTful APIs

11 Jan 2012

Overview

While implementing the API for a recent project we used "named endpoints" instead of hard-coded URLs for the various actions which can be performed via the API [1]. In a nutshell, this means all clients agree to use whatever URL is returned from a named endpoint in your responses. This gives you a lot of freedom to switch out your application's URL structure and introduce optimizations without breaking clients. After working with this for a couple of months, I think I prefer this setup over working with and adding functionality to services on applications using hard-coded URLs.

The purpose of this article is to show a few examples illustrating this approach and explain how we implemented it in our application.

Our implementation

First, we had to come up with the first swath of functionality we wanted to implement and agree on names for the actions we wanted to expose over the API. In our situation, we tried to standardize a few things, such as "When you want to get a JSON representation of a resource from the server, let's use retrieve_(resource_name_here)". This isn't necessarily an absolute, but, it's a convention we tried to stick with. Same goes for "making things" equating to create_(resource_name_here). Downloading and uploading actual files was upload_(resource_name_here) and download_(resource_name_here), etc... you get the point.

Anyway, so we knocked out a few endpoint names we could agree on and decided to go with a response structure along the lines of:

{
  "content": {
    "resource": {
      "attribute1": "attribute 1 value"
      "links": { //link specific to this resource
        "link_name_here": {
          "url": "http://url.com/resource/here",
          "docs": "http://relishapp.com/documentation/for/link-name-here-endpoint"
        },
        "other_link_name": {
          "url": "http://url.com/resource/overthere",
          "docs": "http://relishapp.com/documentation/for/other-link-name-endpoint"
        }
      }
    },
    "links": { //convenience links available on this page
      "endpoint-1": {
        "url": "http://url.com/resource/here",
        "docs": "http://relishapp.com/documentation/for/link-name-here-endpoint"
      }
    },
    "tx_meta": {
      "errors": ["Any transaction-level error metadata we may want to pass down."],
      "developer_notes": ["Things the developer may want to listen for."]
    }
  }
}

Most of this is relatively standard stuff. Some things to note:

  1. All links attributes return JSON objects, each of which have at least both a url and docs key present. The URL key is the URL you'd hit to carry out the action for the named endpoint it belongs to. The docs key contains a link the developer can visit to read about what the functionality and requirements of the endpoint. We ended up using RelishApp for our documentation, but I won't get into those details in this post.
  2. We had a tx_meta JSON object as part of every request which returned a response body, which could be used to store extra details. The details could be stored under:
    1. errors: A container for detailed reasons why a request failed in case the status code alone was not sufficient.
    2. developer_notes: Messages intended for the developers of client libraries which they may find useful to tie into. For example, we could return a deprecation message for actions or an entire API version which on all requests. A developer could have a test which would fail when our "deprecation" message was present. This way, they wouldn't have to worry about "watching our blog" or whatever to find out when a version of their library/client was approaching an "unsupported" state.

Additionally, the "base endpoint" of our API (ie: the "root URL") had a structure along these lines:

{
  "content": {
    "links": {
      "retrieve_resource_x": {
        "url": "/api/resource-type",
        "docs": "https://www.relishapp.com/account/project-name/docs/retrieving-resource-type-x"
      },
      "create_resource_x": {...you get the point}
    },
    "tx_meta:" {...}
  }
}

So clients would hit this and always work from our endpoint names instead of assuming a URL structure based on, say, the resource type.

Impressions so far

So far, our experience has been really positive. On the service-side (API), we can ruthlessly refactor our URL structures as we see fit with zero concerns about breaking our clients. If we wanted to start pointing some of the "download content" URLs to a different URL (say, a CDN), we could do so without any concerns.[2] Additionally, there is just something that feels nice about not having to discuss the URLs your clients need to deal with whenever you think you'd like to change something around a bit.[3]

We didn't use the JSON Hypermedia link relation structure outlined by Mike Amundsen here.[4]. The biggest reason for this is that I hadn't run across it until we had shipped already and I wasn't sure enough about making such a sweeping change to our API response structure without digging in and playing with it a bit more first. Essentially, what we had was working quite well and I didn't see a significant benefit relative to the changes it would require. However, I am planning on doing it at some point and doing it won't break our clients since we versioned our API and lock our client libraries when we ship official versions. Those details will come in another article.

This is a new approach for me and I'd love to hear feedback. Like I said, right now it feels nice, but time will tell.

Thoughts?

I'd love to hear your thoughts. How does it sound to you? Have you done it before? Did you like or regret it down the road?

Notes

  1. This is one of the concepts Jon Moore outlined in his "Hypermedia APIs" talk and Roy Fielding outlined in a blog post from 2008 (or, at least it is how I interpret the 4th bullet point in that article). I've run across several articles which seem to touch on something similar to this approach (maybe using a different response structure, but the same idea of abstracting your application URLs behind a name). Steve Klabnik recently posted about using the rel attribute on links instead of IDs, classes, text, et cetera, explaining how it affected testing via Cucumber on a "regular old" HTML page. My point is, this isn't a new concept or approach, but using it in an API was new to me when this project started (I had experimented with the method Steve outlines in his recent blog post)...
  2. Steve Klabnik sent this link over to me. Thanks, Steve!
  3. Not that I would justify creating "dead" URLs. You should set up redirects on your server when appropriate.
  4. Provided any request requirements (authentication, headers, etc) are matched/supported on the new service.

Inspiration

This article was inspired by experiences I had while working on the RAM project. If you found this interesting, you may want to check out some of the other articles associated with it.