There are countless books, websites and other resources that talk about how RESTful APIs should be designed. So naturally I have to add to the pile.

Stick to Standards

Whenever faced with a new problem, chances are smarter people did solve it already and formulated a standard in some way. I'm a huge fan of standards for several reasons:

  • no bikeshedding
  • there might be tools and libraries that save tremendous amounts of work
  • the standard probably already covers problems we haven't even thought of yet
  • faster onboarding and less documentation required
  • even if we don't completely adhere to the standard it's easier to explain "X without Y plus Z" than start from scratch

In case of RESTful APIs one of the biggest standards probably is JSON:API. I really like it a lot, although most of the time I don't follow it 100%. Usually I stick to simple objects instead of resource objects for example.

Don't Return Flat Payloads

Let's say we want to return a simple payload like this:

{ "key": "value" }

Instead we wrap the whole thing in an object under the data key:

{ "data": { "key": "value" } }

This is actually part of JSON:API, where requests and responses are well defined objects.

Let's have a look at a full example to see why this is pretty neat:

{
  "data": [
    {
      "title": "Tomato Cucumber Salad",
      "tags": ["cucumber", "tomato"]
    },
    {
      "title": "Tomato Salad",
      "tags": ["tomato"]
    },
  ],
  "included": {
    "tags": [
      {
        "name": "cucumber",
        "color": "green"
      },
      {
        "name": "tomato",
        "color": "red"
      },
      {
        "name": "carrot",
        "color": "yellow"
      }
    ]
  },
  "meta": {
    "totalPages": 3,
    "perPage: 2
  }
}

So what did we gain here?

  • data holds only the resources that were actually requested - the client can ignore everything else if wanted
  • included holds embedded data, related to data
  • meta holds all meta data
  • data.tags can be resolved without any further requests by looking at included.tags
  • all information to add a new resource to the collection (what tags are available?) is directly available
  • pagination information is present in meta

Everything has its place and everything is in its place.

Resources Aren't Domain Models

Often it's easy to fall into the trap of thinking API resources represent domain models, or that there must be direct one to one relation between the two.

Thinking like that leads to tight coupling between what the API looks and how it works and the internal workings of our application. Whenever the API changes our domain models will have to change and the other way around.

Should we ever find ourselves in a situation where we have to implemented an API that is dictated by outside requirements in addition to our own existing APIs, things can get pretty hairy. Our domain models would have to fit two possibly wildly differing sets of resources.

Let's say this is what our internal endpoint GET /internal/users/1 returns:

{
  "data": {
    "id": 1,
    "username": "alice",
    "favoriteColor": "blue",
    "street": "fakestreet 123",
    "city": "fakecity",
    "zip": "12345"
  }
}

Now we have to provide an external API for our business partner BigCo Inc. They hand us a several hundred pages long document that describes BigCoAPI which includes GET /external/profiles/1:

{
  "userId": 1,
  "profile": {
    "name": "alice",
    "favouriteColours": ["#00f"],
  },
  "addresses": {
    "home": [
      {
        "street": "fakestreet",
        "streetNumber": "123",
        "city": "fakecity",
        "zip": "12345"
      }
    ]
  }
}

Clearly it would be next to impossible to design our domain models in a way that fits both representations. To make our lives easier it's always a good idea to have a mapping layer between our APIs and our business logic. That way we not only keep the two sides separate in our heads, but in our code, too.