Specify Your API

Much of the past 6 years of my professional life has been spent designing, implementing and maintaining APIs. I've learned many things to avoid as well as a handful of good techniques. However, it wasn't until 3 months ago that I was exposed to a practice I consider the most important thing you can do while building APIs. Every production-quality API I build going forward will be rigidly and explicitly specified.

What does it mean to specify an API? In the abstract, it mostly means writing things down as you make design decisions and doing so in a machine-parsable format. In practice, it means using either Open API (fka Swagger) or JSON Schema.

This post doesn't advocate for a particular tool but rather the practice of specification itself.

Quick Example

Suppose your API exposes a /users resource with the standard create, index and show actions. Below is an example of what a specification for this resource could look like using Open API.

definitions:  
  error:
    type: object
    required:
      - code
      - user_message
      - developer_message
    properties:
      code:
        type: integer
      user_message:
        type: string
      developer_message:
        type: string

  user:
    type: object
    required:
      - id
      - name
    properties:
      id:
        type: string
      name:
        type: string
      age:
        type: integer

paths:  
  /users:
    get:
      summary: Get a list of users
      responses:
        200:
          description: Successful response
          schema:
            type: array
            items: "#/definitions/user"
        default:
          description: Unsuccessful response
          schema: "#/definitions/error"

    post:
      summary: Create a user
      parameters:
        - name: body
          in: body
          required: true
          schema:
            type: object
            required:
              - name
            properties:
              name:
                type: string
              age:
                type: integer
      responses:
        201:
          description: Successful response
          schema: "#/definitions/user"
        default:
          description: Unsuccessful response
          schema: "#/definitions/error"

  /users/{id}:
    get:
      summary: Get a user
      parameters:
        - name: id
          in: path
          required: true
          type: string
      responses:
        200:
          description: Successful response
          schema:
            type: array
            items: "#/definitions/user"
        default:
          description: Unsuccessful response
          schema: "#/definitions/error"

The details of the specification aren't terribly important but you should note a few things:

  • Endpoints are defined via a combination of paths and HTTP verbs
  • Responses are defined for a given endpoint based on status code
  • Schemas can be provided for both requests and responses
  • Schemas can be shared by giving them a name in the definitions object

Why Should I Do This?

The biggest benefit derived from specifying an API is actually writing down the functionality. I can't count the number of times an idea seemed reasonable to me until I actually wrote out the definition for the endpoint and, upon seeing it, recognized all sorts of issues with the design. Think about this as TDD for API design. Write down the API you want before you build it.

After you've written down your APIs functionality, there are all sorts of cool things you can do with it. Two of the biggest problems this solves are request / response validation and documentation generation.

We can use the specification to build a validation middleware for our API's requests and responses. Many people have already done this. Once we're validating our requests and responses based on the specification and we build a solid set of API tests to exercise our API, we're effectively testing the specification. This means the specification can be treated as a source of truth for our API's functionality.

The specification is also extremely useful when producing documentation for our API. Because we're running our test suite through the specification, we have an accurate, up-to-date data source from which to generate an API reference.

Last, but not least, specifying an API leads to much greater consistency. A developer adding a new endpoint will automatically reuse the same error response that other endpoints are using. A developer adding a new endpoint that exposes a user will use the existing user definition. This leads to a consistent representation of a given resource across our entire API. It's actually harder to do the wrong thing -- you'd have to go out of your way to make your endpoint return something non-standard. It's hard to quantify how important this is, but trust me, it's very important.

Next time you build an API, take a bit of extra time up front to specify it. I promise you it'll be worth the effort.