Mario Alberto Chávez Cárdenas

Blog personal de fotografía y desarrollo de software

desarrollo

Keep your API in shape with API Blueprint

Keep your API in shape with API Blueprint

When you start a new API project with Rails, your first question might be, “should I do it using GraphQL or go with good old REST API?”

After much consideration, if you decide to go with a REST API, continue reading this post.

Now, before you begin to consider how to implement your API, your primary concern should be how to keep your API documentation up to date.

It is not an easy task for the development team to keep code in sync with documentation without going thru many hoops.

At a recent project, I was looking for a solution that could easily integrate with Rails and, at the same time, allows the team to document the API definition without considerations about the implementation details.

The first tool that I found was the rspec_api_documentation gem, but it was incompatible with our project for two reasons: We use Minitest, and it requires you to write Specs with Ruby code to describe the API.

Then I remember that in a project for one of our clients at michelada, Apiary was used as a communication tool to describe the API with the team.

API Blueprint

Apiary has an Open Source format called API Blueprint. It is a specification on top of Markdown that helps to describe a web API. This format focuses on creating documentation.

FORMAT: 1A
HOST: http://polls.apiblueprint.org/
# Sample API
Polls is a simple API allowing consumers to view polls and vote in them.
## Questions Collection [/questions]
### List All Questions [GET]
+ Response 200 (application/json)
        [
            {
                "question": "Favourite programming language?",
                "published_at": "2015-08-05T08:40:51.620Z",
                "choices": [
                    {
                        "choice": "Swift",
                        "votes": 2048
                    }, {
                        "choice": "Python",
                        "votes": 1024
                    }, {
                        "choice": "Objective-C",
                        "votes": 512
                    }, {
                        "choice": "Ruby",
                        "votes": 256
                    }
                ]
            }
        ]

With this specification, you can define how the API receives a request and what to expect as a response. The following example shows how Markdown and a special syntax MSON works together to document an API endpoint. No implementation details are present.

MSON syntax allows you to describe JSON objects easily no matter how complicated your JSON object is. An object structure with MSON like the following:

- address
    - street
    - city
    - state

Produces a JSON object like:

{
    "address" : {
        "street": "",
        "city": "",
        "state": ""
    }
}

The format is relatively simple to get up to speed quickly and write a complete set of API documentation. With the API Blueprint format is also possible to generate JSON Schema draft 4 from it. The schemas are useful to validate your API using Minitest.

Generate good-looking documentation.

Let’s start with the low hang fruit of using API Blueprint to document our API. Here is the documentation for two API endpoints; they are our example for the rest of this post.

The API is simple, but it shows some benefits of MSON syntax, like the ability to reuse data structures. It also shows how to document a response that can have many different statuses.

Aglio is a tool that uses the API Blueprint format to render good-looking documentation from it. It has a few available themes that you can choose from, or even better; you can write your own.

In a Rails application, to install aglio use Yarn.

$ bin/yarn add aglio

Aglio command needs at least two parameters, the input, where is the documentation file written with API Blueprint, and the output file for the HTML documentation.

$ bin/yarn run aglio -i docs/api/documentation.md -o public/documentation.html

It produces the HTML documentation with aglio’s default theme. You can pass a set of –theme flags to choose a theme, color scheme, layout, and a few things more, check aglio’s documentation for details.

Start your Rails server and point your browser to http://localhost:3000/documentation.html. There you have a nicely documented API.

It is handy to have a rake task to refresh the HTML documentation anytime with the command bin/rails api:documentation. Create a file lib/tasks/api.rake and add the following lines:

namespace :api do
  desc 'Build API documentation'
  task :documentation do
    input_file = 'docs/api/documentation.md'
    output_file = 'public/documentation.html'
    system(" bin/yarn run aglio -i #{input_file}  -o #{output_file}")
  end
end

Generating JSON Schemas

Now that you have nice looking documentation, it is time to generate JSON Schemas from the same documentation file. The generated files, along with Minitest, help us to automate the verification that the server requests/response work as expected.

To generate the schema files from the API Blueprint file, we need a tool called apib2json. It produces a single JSON file for all schemas present in the documentation, this is fine. Still, for our needs, a single file does not work, so we need an additional step to split this file into multiple files, each one with a single schema representation.

Unfortunately, the way that apib2json works, it does not provide enough metadata to help us identify each generated schema to put it into a file. So you need a custom version of apib2json, which you can find at https://github.com/mariochavez/apib2json/tree/additional-metadata.

To install it, use Yarn.

$ bin/yarn add "https://github.com/mariochavez/apib2json.git#additional-metadata"

Then execute it to generate a single schema file. The command needs an input, which is the API Blueprint file, and an output file for the JSON schemas; the --pretty flag is for us to get a human-readable file.

$ bin/yarn run apib2json --pretty -i docs/api/documentation.md  -o test/support/schemas/schemas.json

{
  "[GET]/users{?cursor}": [
    {
      "meta": {
        "type": "response",
        "title": ""
      },
      "schema": {
        "$schema": "http://json-schema.org/draft-04/schema#",
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "email": {
                  "type": "string",
                  "description": "User's email."
...

With the help of a second rake task, the generated file is divided into multiple files; each file will contain one request with a single response. A file GET-Users-200.json contains a request with a GET to Users endpoint with a 200 status response.

Create the file lib/tasks/api.rake and add the following code.

desc "Generate JSON schemas"
  task :schemas do
    schemas_path = "test/support/schemas"
    input_file = "docs/api/documentation.md"
    output_file = "test/support/schemas/schemas.json"
    puts "Generating api schemas from #{input_file}"
    system("bin/yarn run apib2json --pretty -i #{input_file} -o #{output_file}")
    if File.exist?(output_file)
      file_path = Pathname.new(output_file)
      JSON.parse(file_path.read).each_pair do |group, actions|
        actions.each do |action|
          next if action.dig("meta", "type") != "response"
          verb = group.scan(/\[(.*)\]/).flatten.first
          name = "#{verb}-#{I18n.transliterate(action.dig('meta', 'group'))}(#{action.dig('meta', 'statusCode')})#{action.dig('meta', 'title')&.gsub(/ /, '-')}".
            sub(/\{.*\}/, "").gsub(/\(|\)/, "-").gsub(/^-|-$/, "")
          puts "Writing #{name}"
          File.open("#{schemas_path}/#{name}.json", "w") { |file| file.write(action.dig("schema").to_json) }
        end
      end
    end
    puts "Schemas are ready at #{schemas_path}"
  end

To execute the rake task, run the following command.

$ bin/rails api:schemas
Generating api schemas from docs/api/documentation.md
yarn run v1.7.0
$ node_modules/.bin/apib2json --pretty -i docs/api/documentation.md -o test/support/schemas/schemas.json
Done in 0.31s.
Writing GET-Users-200
Writing POST-Users-201
Writing POST-Users-422
Schemas are ready at test/support/schemas

There you have it, one JSON schema file per request/response. The rest is quite simple, add the json_matchers gem in your Gemfile and configure the test_helper.rb to load the gem.

require 'json_matchers/minitest/assertions'
JsonMatchers.schema_root = 'test/support/schemas'
Minitest::Test.send(:include, JsonMatchers::Minitest::Assertions)

Then create an integration test for a UsersController and add the following tests. Each test will perform a request to the API and match the response accordingly to what you documented in the API Blueprint file.

require 'test_helper'
class UsersApiTest < ActionDispatch::IntegrationTest
  test 'Users List' do
    get '/users', headers: { Accept: 'application/vnd.api-test.v1+json' }
    assert_response :success
    assert_matches_json_schema response, 'GET-Users-200'
  end
  test 'Create new User successfully' do
    post '/users', headers: { Accept: 'application/vnd.api-test.v1+json' }, params: user_payload
    assert_response :created
    assert_matches_json_schema response, 'POST-Users-201'
  end
  test 'Fails to create new User' do
    post '/users', headers: { Accept: 'application/vnd.api-test.v1+json' },
                   params: user_payload(email: nil, first_name: nil)
    assert_response :unprocessable_entity
    assert_matches_json_schema response, 'POST-Users-422'
  end
  def user_payload(attrs = {})
    {
      email: '[email protected]',
      first_name: 'Jane',
      last_name: 'Doe'
    }.merge(attrs)
  end
end

If there is a mismatch between the request and the documented response, the test will fail with detailed information on why the response differs from the JSON schema.

#: failed schema #: "links" wasn't supplied.
---
expected
{
}
to match schema "POST-Users-201":
{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "type": "object",
  "properties": {
    "data": {
      "type": "array",
      "items": {
...

Conclusion

Keeping your API documentation and endpoints in sync can be a daunting task; writing schema files by hand are not fun at all, but hopefully, with the tools presented here, it will be easy for you and your team to keep everything up to date.

If you want to check a sample application with everything showed in this post to go https://gitlab.com/mariochavez/testing-api-blueprint/tree/master.