We learn as we go, we write as we learn.

About michelada.io


By joining forces with your team, we can help with your Ruby on Rails and Javascript work. Or, if it works better for you, we can even become your team! From e-Commerce to Fintech, for years we have helped turning our clients’ great ideas into successful business.

Go to our website for more info.

Tags


Keep your API in shape with API Blueprint

2nd August 2018

When you are starting a new API project with Rails your first question might be: should you do it using GraphQL or just go with a good old REST API.

After much consideration, you decide to go with a REST API? Good! Continue reading this post.

Now, before you begin to consider how to implement your API, your main concern should be how to keep your API documentation up to date. This is something that always creates a sort of tension in projects where the team have to go through many hoops to keep code and docs in sync.

In a recent project, I was looking for a solution that could be easily integrated into our Rails application and at the same time allow the team to write the API definition without caring 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 we did use Apiary as a communication tool to describe the API implemented there.

API Blueprint

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

Screen-Shot-2018-06-08-at-1.25.57-PM

With this specification, you can define how a request should be made and what to expect as a response from an API. Here is a sample fragment of API Blueprint that shows how to document the /questions endpoint, no code just Markdown and a special syntax MSON to document the response.

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
                    }
                ]
            }
        ]

MSON syntax allow us to describe JSON objects in a very simple way no matter how complicated you 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 fairly simple to get up to speed quickly and write a complete set of API documentation. From the format is also possible to generate JSON Schema draft 4 for us, we can use these schemas to validate our API with Minitest.

Generating 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 that we are going to use in this post as an example.

The API is simple but it shows nice things about the MSON syntax like data structures reuse. It also shows how to document a response that can have many different statuses.

FORMAT: 1A
HOST: https://api-test.com

# Sample API documentation
We just use Markdown with special syntax to document our API.

# Group Users
A User is a representation of a User on the system.

## Users [/users{?cursor}]
Endpoint for users.

### List Users [GET]
Returns a list of users paginated by the cursor.

+ Parameters
    + cursor: `10` (number, optional) - Cursor value to paginate response.

+ Request (application/json)
    + Headers

            Accept: application/vnd.api-test.v1+json

+ Response 200 (application/json)

    + Attributes
        + data (array[User], fixed-type) - Users data.
        + pagination (object, required) - Pagination information.
            + cursors (object, required) - Cursors.
                + after: `10` (number, required) - Cursor for next record to fetch.
                + next_uri: `/users?cursor=5` (string, required) - URI for next page.
        + links (array, fixed-type, required) - Links references.
            + (object)
                + rel: `self` (string, required)
                + uri: `/users` (string, required)

### Create User [POST]
Creates a new User.

+ Request (application/json)
    + Headers

            Accept: application/vnd.api-test.v1+json

    + Attributes (User Base)

+ Response 201 (application/json)

    + Attributes
        + data (array[User], fixed-type) - Users data.
        + links (array, fixed-type, required) - Links references.
            + (object)
                + rel: `self` (string, required)
                + uri: `/users/1` (string, required)

+ Response 422 (application/json)

    + Attributes
        + data (array[User Base], fixed-type) - Users data.
        + errors (array, fixed-type, required) - Action error.
            + (object)
                + type: `blank` (string, required) - Type of validation error.
                + code: `ERR-100` (string, required) - Error unique code.
                + message: `email can't be blank.` (string, required) - Error descriptive message.
                + href: `http://api-test.io/api/documentation` (string, required)
                  Link to learn more about the error.



# Data Structures

# User Base (object)
+ email: `user@mail.com` (string) - User's email.
+ first_name: `Jane` (string, required) - User's first name.
+ last_name: `Doe` (string, required) - User's last name.

# User (User Base)
+ id (number) - User's ID.

To render an HTML document of our documentation we can use a tool called aglio. It parses the file and generates a nice looking document. You can choose any of the available themes or you can write your own.

In a Rails application, we install aglio with Yarn as follows

$ bin/yarn add aglio

The basic command, given you saved the documentation file in the docs/api folder and want to output the HTML to the /public folder, would be:

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

This will produce an HTML with aglio's default theme. You can pass additional options to choose a theme or color scheme, for more details check aglio's documentation.

If you start the Rails server and point your browser to http://localhost:3000/documentation.html you will see your API nicely documented.

Screen-Shot-2018-06-06-at-4.22.20-PM

Let's add a rake task to our application to generate this file by running 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 we have nice looking documentation its time to generate JSON Schemas from the documentation, with these schema files we can use Minitest to automate the verification that our API is responding in the format we expect.

There is a tool called api2bjon that does the extraction of JSON Schemas from API Blueprint documentation. It generates a single JSON file for all schemas present in the documentation.

To generate the file with all schemas first, install apib2json and then execute it.

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

The following is a fragment of the generated file:

{
  "[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."

There is a problem with this file since all possible schemas are writing into this file we will need to parse the file and put each schema into its own file so we can use them with Minitest.

Unfortunately, the metadata provided by apib2json as it is, is not enough to separate schemas into independent files with unique naming. An improved version is available at https://github.com/mariochavez/apib2json/tree/additional-metadata.

To install apib2json from this repository execute the following command.

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

Now if you regenerate the schemas file its metadata will contain additional information.

{
  "[GET]/users{?cursor}": [
    {
      "meta": {
        "type": "response",
        "title": null,
        "group": "Users",
        "statusCode": "200"
      },
      "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."

But as I said before, for this file be useful we need to split it to have each schema in their own file. To accomplish this let's create a rake task. Open 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

Executing this task will generate the schemas.json file from the API Blueprint document but it will also split the file into many files, each one containing a schema for a single response. Files will be named with the HTTP Verb, the resource, request name if any, and the HTTP status code for example: GET-Users-200.

$ 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

You are ready to start writing tests with Minitest in your Rails application. First, install the json_matchers gem in your Gemfile. Configure your test_helper.rb file to load json_matchers.

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 to verify all the three responses are 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: 'user@mail.com',
      first_name: 'Jane',
      last_name: 'Doe'
    }.merge(attrs)
  end
end

The key here is the assertion for a JSON Schema. If your endpoint is not responding with the expected format then you get detailed information from the test telling you what are you missing, what was expected and what was your response.

#: 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 endopoints 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

View Comments