API Documentation Made Easy

I have been doing a little fun side project for myself on and off for the better part of six months. The end goal is to complete it for it to be useful, but as of late it’s been more of a playground for myself to learn new technologies and step out of my comfort zone.

Currently I have been focused on creating an API for the application utilizing the JSON:API spec backed by a ruby back-end using the JSON API-RB gem. I was breezing through setting it up until I took a break for a month and subsequently tried to get back into it during my sabbatical. This is where I hit snags in re-learning where I left off.

Documentation is key

The main issue I had with getting back to the project was trying to remember what I had done or what was left to do. I had plenty of specs written, but there was something missing. It struck me that I could use some form of documentation — even if it wasn’t to be made public — to outline where the gaps were. I set off trying to figure out the best course of action to get documentation in play.

What to use…what to use…

At my current gig we utilize Grape with Swagger. This works for us because the app is so big and there are many touch points and having an opinionated setup helps streamline things. My app is currently super small, I am the only one working on it, and I was already using serializers to manage my representations. Just glancing through initial setup for Swagger docs I could already tell it would be overkill for what I need.

I looked back at my well documented specs and thought there must be a way to utilize these in some form. A google search landed me on an RSpec API Documentation gem. It was exactly what I needed to keep things simple and utilize what I had at hand. Granted just adding the gem wouldn’t solve it right away without some intervention on my side, but this was another experience to learn something new. I even was able to make a contribution to some open source.

Key findings

Below is an example pulled from one of my specs written, outlining some of the different options provided by the documentation gem.

1
post "/api/v1/bank_account" do
2
  it_behaves_like "an endpoint with correct errors"
3
4
  let(:name) { "Sloan" }
5
  let(:family_id) { family.id }
6
  let(:child_id) { child.id }
7
8
  with_options scope: :data,
9
               required: true,
10
               with_example: true do
11
    parameter :family_id, "The family ID"
12
    parameter :child_id, "The ID of the child bank account associated with"
13
    parameter :name, "The name of the bank account"
14
  end
15
16
  example_request "POST: Bank Account" do
17
    expect(status).to eq(200)
18
    expect(document["data"]).to have_attribute(:child_id).with_value(child.id)
19
    expect(document["data"]).to have_attribute(:name).with_value("Sloan")
20
    expect(document["data"]).to have_attribute(:family_id).with_value(family.id)
21
  end
22
end

One of the things I found was how much cleaner my tests had become. Overtime, I was able to refactor the tests down so that they covered only what was needed and they were clear on what they were testing. The documentation provided on the GitHub page was super helpful with clear concise examples.

I ended up creating a few shared examples to for the error cases so that each endpoint could be covered in that direction. Those examples are depicted below:

1
shared_examples_for "an endpoint with correct errors" do |params|
2
  example "returns a 401 error message", document: false do
3
    if user
4
      header "access-token", nil
5
      header "client", nil
6
      header "token-type", nil
7
      header "expiry", nil
8
      header "uid", nil
9
      do_request
10
      expect(status).to eq 401
11
      expect(JSON.parse(response_body, object_class: OpenStruct)["errors"]).to include I18n.t("devise.failure.unauthenticated")
12
    end
13
  end
14
15
  example "returns a 422 message", document: false do
16
    do_request(data: { foo: "bar" })
17
    expect(status).to eq 422
18
  end
19
20
  example "returns a 404 message", document: false do
21
    if params.present?
22
      do_request(params)
23
      expect(status).to eq 404
24
    end
25
  end
26
end

Being able to outline parameters, required or not, which included the description was very helpful to remind myself visually what was needed to send across the wire. There are plenty of other options provided by the gem I have not used to flesh out the docs, but this was an good start.

A few discoveries and modifications

Since I am using Devise Token Auth and all the logged in endpoints require authentication, I needed to figure it a way to keep things dry in the tests. Instead of typing out the required headers each time, I settled for creating a before each filter to do just that which I included in my rails helper file for the Rspec.config block.

1
config.before(:each, type: :doc) do
2
  unless user.nil?
3
    user.create_new_auth_token.each do |key, value|
4
      header key, value
5
    end
6
  end
7
end

This will loop through all the keys in the auth token for a user and print them out as headers in the spec before the request is run.

I also found the rake task provided by the gem to generate the docs to be somewhat limiting. By default it is keyed to the acceptance directory for specs. I had opted for doing the Rails 5 convention and putting all my request specs in the features directory.

Since the gem could not look into that directory, I forked their repository to make a modification so that an option string could be passed through to change the directory.

1
rake docs:generate[MYFOLDER] # generate docs from specified folder

Now any directory can be passed through to make sure the specs are generated. Also, it is setup so that if no option passed in, it will default to the original acceptance directory.

The Outcome

One of the settings in the gem initializer file allows the specification of the type of file produced. By default it is set to HTML, but I was not going to serve these up to look at through a server. Instead, I changed the option to generate Markdown and installed a VSCode markdown renderer plugin. As an added benefit, the GitLab repository renders the Markdown pages just like a wiki would, so viewing in browser comes for free!

After this little side jaunt, I can definitely say I have learned some new tools of the trade. I now can look back and see what state my API is in and how visually things are hooked together. Documentation is hard to write sometimes, but when there is a tool to auto generate this, definitely take advantage.

Filed under: Code