7 min read

Designing an API from scratch is a frustrating affair; there are a million best-practices to keep in mind, granularity to argue over, and the inherent struggle between consumer & developer. This article deals with all of the issues above, by concretely developing an API for a courier service–addressing the needs of the business, how they translate into data relationships & schema, and how they could/should be translated into consumable REST end-points.

Make your life easy

There is a shelf of software; take from it greedily!

Instead of busting your hump to design your own API spec from scratch, make your life easy, and look to the world for already validated and accepted API specs. I advocate heavily for JSON:API, as I believe it offers the best combination of best-practices and granularity (I’d call it medium to medium-fine grained), thus we’ll be writing our API out according to the JSON:API spec.

If you are a masochist and/or truly wish to design your own API spec then I would highly suggest wisely brushing up on the state-of-the-art, via the following articles:

Using HTTP Methods for RESTful Services
REST API Design – Resource Modeling
CQRS summary

Design step 1: determine business concerns

Before we even begin coding, we need to identify our business concerns. These will lead us to our resources, and thus the resources our API consumers will want/need to deal with.

It is very important that we do not confuse necessary business logic/resources with necessary consumer endpoints.

As far as the API consumer is concerned, they should be given a tool to perform introspection on the state of their affairs, and no one else’s. This means that they should be able to command new deliveries and inspect the state of current & past deliveries; they should not have insight into any other aspects of our operation.

Nouns and their actions

Because we’re dealing with a courier service, try to estimate the consumer-facing nouns, and their relevant actions:

  • Customers
    • Command the delivery of a package, specifying origin and destination
  • Couriers
    • Receive and deliver packages
  • Packages
    • Get delivered from a sender to a recipient by a courier
  • Senders
    • Hand the package off to the Courier
  • Recipients
    • Receive the package from the Courier

In performing this exercise, we’ve hopefully made clear the relationship between customers, couriers, packages, senders, and recipients.

Not all nouns are equal

You may have noticed that couriers, senders, and recipients appear to be cyclically linked. When relationships like this occur, it likely means that these nouns (pieces of data) do not deserve to be independent resources, but rather properties of another resource–specifically, packages.

Be sure to keep an eye out for this, as you no doubt will be tempted to make all of your nouns resources (albeit heavily related ones), a practice that will bloat your API.

Design step 2: formalize relationships

Armed with your list of resources, nouns, and their actions, formalizing the relationship between resources should follow naturally. In our case, because we identified a cyclical relationship, we were able to consolidate our nouns into only two resources: customers and packages.

The relationship between these (customers and packages) is thankfully quite obvious: one-to-many. That is because each customer can have any number of packages while a package may only belong to one customer.

Internally, we’ve already collapsed the relationship between packages and couriers, senders, & recipients by making them simple properties–implicitly identifying these as one-to-one relationships.

Design step 3: formalize the schema

A dovetail to step 2, we now must concretely start listing the properties of each resource.

customer: {
  id: "string",         //UUID
  packages: ["string"]  //Array of UUIDs
}
package: {
  id: "string",           //UUID
  origin: "string",       //Address
  destination: "string",  //Address
  customer: "string",     //UUID
  sender: "string",       //Name
  recipient: "string",    //Name
  courier: "string"       //UUID
}

As we mentioned in step 2, the relationship between customers and packages is one-to-many–this is represented by the array of package ids–and each package has the consolidated nouns as properties.

Design step 4: mock it out

It is critical to use authorization tokens to avoid leaking package and customer information to the wrong parties. That said, for the case of this step, we are primarily concerned with mocking the API via JSON:API spec to see how it looks and feels.

If you want a motivated example, just think of the case of whenever anyone queries http://example.com/customers. If the whole world has access to the collection of every customer you’ve got a huge problem.

You can see an interactive example here–built off of a RAML file.

Example:

GET http://example.com/packages =>

{
  "links": {
    "self": "http://example.com/packages"
  },
  "data": [{
    "type": "packages",
    "id": "1",
    "origin": "1600 Pennsylvania Ave NW, Washington, DC 20500",
    "destination": "2 Lincoln Memorial Cir NW, Washington, DC 20037",
    "sender": "Barry Obama",
    "recipient": "Abraham Lincoln",
    "courier": "1",
    "links": {
      "self": "http://example.com/packages/1",
      "customer": {
        "self": "http://example.com/packages/1/links/customer",
        "related": "http://example.com/packages/1/customer",
        "linkage": { "type": "customers", "id": "1" }
      }
    }
  },
  {
    "type": "packages",
    "id": "2",
    "origin": "437 N Wabash Ave., Chicago, 60611",
    "destination": "111 S Michigan Ave., Chicago, IL 60603",
    "sender": "Donald Trump",
    "recipient": "Marshal Fields",
    "courier": "1",
    "links": {
      "self": "http://example.com/packages/2",
      "customer": {
        "self": "http://example.com/packages/2/links/customer",
        "related": "http://example.com/packages/2/customer",
        "linkage": { "type": "customers", "id": "1" }
      }
    }
  }],
  "included": [{
    "type": "customers",
    "id": "1",
    "links": {
      "self": "http://example.com/customers/1"
    }
  }]
}
GET http://example.com/customers =>

{
  "links": {
    "self": "http://example.com/customers"
  },
  "data": [{
    "type": "customers",
    "id": "1",
    "links": {
      "self": "http://example.com/customers/1",
      "packages": {
        "self": "http://example.com/customers/1/links/packages",
        "related": "http://example.com/customers/1/links/packages",
        "linkage": [
          {"type": "packages", "id": 1},
          {"type": "packages", "id": 2}
        ]
      }
    }
  }],
  "included": [{
    "type": "packages",
    "id": "1",
    "origin": "1600 Pennsylvania Ave NW, Washington, DC 20500",
    "destination": "2 Lincoln Memorial Cir NW, Washington, DC 20037",
    "sender": "Barry Obama",
    "recipient": "Abraham Lincoln",
    "courier": "1",
    "links": {
      "self": "http://example.com/packages/1"
    }
  },
  {
    "type": "packages",
    "id": "2",
    "origin": "437 N Wabash Ave., Chicago, 60611",
    "destination": "111 S Michigan Ave., Chicago, IL 60603",
    "sender": "Donald Trump",
    "recipient": "Marshal Fields",
    "courier": "1",
    "links": {
      "self": "http://example.com/packages/2"
    }
  }]
}

Yes, these responses seem fairly verbose, but they are designed to lower the number of necessary requests per interaction–making your server gather the necessary data you would have called for anyway, in anticipation of those requests rather than just in time.

Believe me, after a few minutes of reading the JSON, you’re visceral reaction won’t lean so strongly towards disgust.

Design step 5: get feedback

Once you have a mock written out that people can try for themselves, like the interactive example in step 4, it’s time for you to ask the hard question: what are the pain points?

Warning: unless you are the sole consumer of your API, try to avoid making changes that conflict with the JSON:API spec. Remember the whole point of using the spec was so to decrease your design overhead, and the consumer’s learning curve.

Design step 6: finalize with tests

While you are waiting for feedback, you should start writing tests. If you spec’d your API with something like RAML or API Blueprint it’s really easy to use your spec files to auto-generate mock API servers–so testing is a breeze.

Conclusion

By following the JSON:API spec we do away with a lot of the head scratching and frustration typically involved in designing an API. What’s left behind is simply the tedious task of implementation, and while that’s not a terribly exciting conclusion, I’ll take that over 10 hours of meetings on whether to use PUT or PATCH, and which color to paint the bike shed.

And who knows? Maybe with your new-found time, you’ll write a JSON:API generator (connected to your favorite frame work) so that next time, you won’t even have to think about implementation either!

About the author

Jonathan Pollack is a full stack developer living in Berlin. He previously worked as a web developer at a public shoe company, and prior to that, worked at a start up that’s trying to build the world’s best pan-cloud virtualization layer. He can be found on Twitter @murphydanger.

LEAVE A REPLY

Please enter your comment!
Please enter your name here