Engineering

API Docs That Automatically Stay Up To Date

Ladder recently rolled out the Ladder API as part of our one year launch anniversary, and with that, we had to decide up front how we were going to tackle keeping documentation in sync with reality.

We wanted to have a solution which could be used to both validate API endpoints and generate our documentation from the same source. This will ensure that docs are a reflection of reality.

Defining Endpoint I/O

We first spec out all input and output, with descriptions alongside in code.

(def descriptions
  {::some_input "A super useful input"
   ::id "The user's id"
   ::number_rating "The score on a 1-5 scale"
   ::score_text "Some response text we'll send you"})

(s/def ::some_input string?)

(s/def ::id string?)
(s/def ::number_rating integer?)
(s/def ::score_text string?)

(s/def ::score (s/keys ::req-un [::number_rating
                                 ::score_text]))

(s/def ::cool-thing-output (s/keys :req-un [::id
                                            ::score]))

We group these into one namespace, making it trivial to find all the inputs and outputs from our api in one place for our engineers.

Tested Examples

We also wanted to ensure correctness in the examples provided with each endpoint. If you haven't seen the talk we gave on Ladder's fullstack testing framework at Clojure West, it's worth checking out. But given that ecosystem we have in place, we were able to fluidly define all our examples in Clojure, and then use them in tests.

(def example-endpoint-io
  {[:post ["/api/do-a-thing"]] {:input {"some_input" "hooray!"}
                                :output {"id" "abc123"
                                         "score" {"number_rating" 5
                                                  "score_text" "Woo!"}}})

We override ids and other non-deterministic I/O to give consistent examples in the docs, but all static inputs are used as-is in testing, meaning that we can ensure as part of CI that all the examples are correct.

Bringing it Together

We define our HTTP endpoints with their handler along with input and output formats which get used for validating the API requests in production.

{:title "Do a Cool Thing"
 :description "This endpoint does a really cool thing. Like, for real."
 :method :post
 :url ["/api/do-a-thing"]
 :input ::cool-thing-input
 :output ::cool-thing-output}

And from the same source, we generate Markdown by recursively walking the specs' structure for :input and :outputand rendering a new table for each field corresponding to a child object type. Afterwards, the examples are pulled in and rendered as JSON inputs and outputs.

# Do a Cool Thing

This endpoint does a really cool thing. Like, for real.

### HTTP Request
`POST` `https://www.ladderlife.com/api/do-a-thing`

### Parameters

Field | Type | Required | Description
----- | ----- |  ----- | -----
`some_input` | `integer` | **req** | A super useful input

### HTTP Response

Field | Type | Required | Description
----- | ----- |  ----- | -----
`id` | `integer` | **req** | The user's id
`score` | `object` | **req** | Object of type `Score`. See below for details.

### Score
Field | Type | Required | Description
----- | ----- |  ----- | -----
`number_rating` | `integer` | **req** | The score on a 1-5 scale
`score_text` | `string` | **req** | Some response text we'll send you

### Example Request

```shell
$ curl "https://www.ladderlife.com/api/v1/do-a-thing"
   -H "Authorization: Bearer your-api-key"
   -H "Content-Type: application/json"
   -X POST
   -d
'{
  "some_input": "hooray!"
}'
```

### Example Response

```json
{
  "id": "abc123",
  "score": {
    "number_rating": 5,
    "score_text": "Woo!"
  }
}
```

A side effect of having nicely spec'ed I/O is that generating good error messages for bad requests becomes trivial.

{
  "status": 400,
  "reason": "Missing top level key `some_input`"
}

A final benefit is that our process integrates beautifully with our CI system. CI picks up any API changes, runs the tests, and generates the static files for the docs so they can be seen in our diffs during code review!

Our markdown files are fed into slate, but it's just for quick styling. At some point we'll outgrow that and have to move to generating html and css in house for more flexibility.

With all of this, we've built a process where changes to functionality are automatically reflected in our docs, which allows us to iterate quickly without worrying about constant audits. It allows engineers with less context to jump in where they otherwise might be scared to make changes, a freedom which is part of our core engineering culture at Ladder!