Generate API Documentation Effortlessly from your Phoenix/Elixir code – Part1

  October 08, 2020

This article is about facts and about technologies that let's you effectively and effortlessly generate beautiful API documentation directly from your Phoenix code. Phoenix is a productive web framework build in Elixir. Elixir is a dynamic, functional language designed for building scalable and maintainable applications that leverages Erlang VM. I know it's a lot of new terms, but don't worry. Eventually we're going to be working with controllers and tests, and we all know the meaning of those two terms.

To give you a short overview what we'll going to do: we're going to use phoenix_swagger library to generate swagger file directly from our controllers. Then we're going to use library called bureaucrat that consumes that swagger file, runs your controller tests and generates a markdown file containing information from both (macros + tests). Finally we're going to use slate, which is a static API documentation renderer, feed it with a generated markdown file and generate a beautiful HTML documentation from it.

PhoenixSwagger

PhoenixSwagger is the library that provides swagger integration to the phoenix web framework. The library currently supports OpenAPI Specification version 2.0 (OAS). Version 3.0 of OAS is not yet supported. To use PhoenixSwagger with a phoenix application just add it to your list of dependencies in the mix.exs file:


def deps do
  [
    {:phoenix_swagger, "~> 0.8"},
    {:ex_json_schema, "~> 0.5"} # optional
  ]
end

ex_json_schema is an optional dependency of PhoenixSwagger but we will need it later, so please install it as well. Now run mix deps.get.

Next add a config entry to your phoenix application specifying the output filename, router and endpoint modules used to generate the swagger file:


config :my_app, :phoenix_swagger,
  swagger_files: %{
    "priv/static/swagger.json" => [
      router: MyAppWeb.Router, 
      endpoint: MyAppWeb.Endpoint
    ]
  }


You can also configure PhoenixSwagger to use Jason as a JSON library. I highly recommend doing this as Jason is a default JSON library for Phoenix and it seems it's winning the battle of adoption over Poison as a primary JSON Elixir library.


config :phoenix_swagger, json_library: Jason

Now we need to create the outline of your swagger document. Imagine the outline as a general information about your API, name, version, TOS, security definitions etc... The outline is implemented as map returned from a function swagger_info/0 defined your Router module.


defmodule MyApp.Router do
  use MyApp.Web, :router


  pipeline :api do 
   plug :accepts, ["json"]
  end

  scope "/api", MyApp do
    pipe_through :api
    resources "/users", UserController
  end 

  def swagger_info do
    %{
      schemes: ["http", "https", "ws", "wss"],
      info: %{
        version: "1.0",
        title: "MyAPI",
        description: "API Documentation for MyAPI v1",
        termsOfService: "Open for public",
        contact: %{
          name: "Vladimir Gorej",
          email: "[email protected]"
        }
      },
      securityDefinitions: %{
        Bearer: %{
          type: "apiKey",
          name: "Authorization",
          description:
          "API Token must be provided via `Authorization: Bearer ` header",
      in: "header"
        }
      },
      consumes: ["application/json"],
      produces: ["application/json"],
      tags: [
        %{name: "Users", description: "User resources"},
      ]
    }
  end
end    


See the Swagger Object specification for details of other information that can be included. Now run the following command and the swagger spec file (swagger.json) will be generated in ./priv/static/ directory.

	
$ mix phx.swagger.generate
	

Of course, at this point, the generated swagger spec file only contains information provided by swagger_info/0 function inside our Router module. To give it actual value we have to decorate our controller actions with PhoenixSwagger macros. I won't go into details here how to do that. phoenix_swagger documentation does a more that adequate job of guiding you to properly create these macros.

	
import Plug.Conn.Status, only: [code: 1]
use PhoenixSwagger

swagger_path :index do
  get("/users")
  description("List of users")
  response(code(:ok), "Success")
end

def index(conn, _params) do
  users = Repo.all(User)
  render(conn, :index, users: users)
end
	

This example demonstrates a really simple decorations example. For more complex macros, consult phoenix_swagger documentation. Also notice that instead of numeric HTTP code in response macro, I'm using a code(:ok) function that converts :ok atom into numeric 200. This is my personal convention. I'd rather read atoms with explicit meaning than HTTP numeric codes, where I always have to think for a while what this code represents.

Unfortunately I bumped into two issues when decorating my controllers with PhoenixSwagger macros.

1.) Sharing common Schemas

PhoenixSwagger doesn't have idiomatic solution how to share common Schemas. Schemas are declarative descriptions of the data structures that the endpoint is returning. To define what common Schema actually is:

Common Schema is a schema used in more than one controller.

This is very important to remember. If you want to keep your Schemas DRY, you have to come up with a way how to achieve sharing your Schema across controllers. By necessity I had to come up with a solution. Initially I've created the issue on PhoenixSwagger repository and then I provided a Pull Request (PR) with complete documentation of the proposal/solution. We're still collaborating with the author of the library inside the Pull Request to come up with the best solutions, but the following is status quo of the proposal:

This is how you would define Schemas for you controller conventionally:

	
  def swagger_definitions do
  %{
    User:
      swagger_schema do
        title("User")
        description("A user of the application")

        properties do
          name(:string, "Users name", required: true)
        end
      end
  }
end
	

This is how you would define Schemas for you controller with common Schema support:

	
  def swagger_definitions do
  create_swagger_definitions(%{
    User:
      swagger_schema do
        title("User")
        description("A user of the application")

        properties do
          name(:string, "Users name", required: true)
        end
      end
  })
end
	

The distinction is subtle, yet very important. Instead of returning the map, we call create_swagger_definitions/1 function, which returns Schemas provided as on only argument to the function merged with common Schemas. For more information about how this works, please look into the PR I mentioned above.

Note: the actual solution may change before the Pull Request is actually merged

2.) No support for nested Phoenix Resources

This was actually a big bummer for me. Phoenix framework supports nested Resources. When creating nested resources, you end up with having action with the same name but different signature inside your controller module.

Router module

	
resources "/groups", GroupController, only: 
[:show] do
resources "/users", UserController, only: 
[:index]
end
	

UserController module

	
swagger_path :index do
  get("/groups/{group_id}/users")
  description("List of users specific to group")
  response(code(:ok), "Success")
end

def index(conn, %{"group_id" => group_id}) do
  ...controller body...
end

# vs

swagger_path :index do
  get("/users")
  description("List of users")
  response(code(:ok), "Success")
end

def index(%Plug.Conn{} = conn, params) do
  ...controller body...
end
	

As you can see we use pattern matching to match the appropriate controller action to router mapping. And herein lies the problem. PhoenixSwagger is not able to create macros for controller action with the same name (:index). The first macro will always match the :index action.


The first thing I did when I found out that this may be problem, I created an issue on PhoenixSwagger repository. Unfortunately I couldn't wait for somebody to provide me with a workaround so I connected the rest of the brain-cells I had available this evening and come up with multiple workarounds myself. One workaround seemed promising so I continued to build on top of it and eventually it became a working theory. Of course I had to compromise. I had to get rid of nested resources in my Router module, but I didn't need to touch my controller files, which was a great win. The magic lies in defdelegate Elixir macro.

Router module

	
resources "/groups", GroupController, only: [:show]
get "/groups/:group_id/users", UserController, :index_by_group
	

UserController module

	
defdelegate index_by_group(conn, params),
  to: UserController,
  as: :index

swagger_path :index_by_group do
  get("/groups/{group_id}/users")
  description("List of users specific to group")
  response(code(:ok), "Success")
end

def index(conn, %{"group_id" => group_id}) do
  ...controller body...
end

# vs

swagger_path :index do
  get("/users")
  description("List of users")
  response(code(:ok), "Success")
end


def index(%Plug.Conn{} = conn, params) do
   ...controller body...
end
	

defdelegate allows me to create local alias for one of the :index functions and PhoenixSwagger macro consumes this delegation. That way PhoenixSwagger thinks it has two distinct controller action names. As I mentioned earlier, you cannot use nested resources anymore, but the nesting was a price I was willing to accept. For more information about the nested Phoenix resources vs PhoenixSwagger, alternate solution or any other important information, please watch the GitHub issue.

All problems solved for now, let's again run following command and your newly generated swagger spec file will contain all the information defined in controller macros translated into JSON representation.

	       
$ mix phx.swagger.generate
	

Now there is final nifty trick in the sleeve of PhoenixSwagger library. The library includes a plug with all static assets required to host SwaggerUI from your Phoenix application. Add a swagger scope to your router, and forward all requests to SwaggerUI:

	
scope "/api/swagger" do
  forward "/", PhoenixSwagger.Plug.SwaggerUI,
    otp_app: :my_app,
    swagger_file: "swagger.json"
end
	

Run the server with mix phx.server and browse to localhost:4000/api/swagger. SwaggerUI should be shown with your swagger spec file loaded.

In order to access you swagger spec file without SwaggerUI just access the following URL: localhost:4000/api/swagger/swagger.json.

Now you understand major capabilities and limitations of PhoenixSwagger library. If you got here, you already know how to utilize it and generate API documentation directly from your code. Personally, PhoenixSwagger macros helps me when I quickly need to understand what the controller does and what status codes it returns without actually reading the controller code, but rather looking at controller macros.

The only thing that is missing now is lack of API call examples (Request/Response pairs) inside our generated swagger spec file for every phoenix controller action that we decorated with macros. Yes, SwaggerUI will allow us to make actual HTTP Requests against our API but that is insufficient. And this is exactly where bureaucrat comes in. It integrates effortlessly into our existing solution. But that's story for another time and part 2 of this series.

Tip 1

PhoenixSwagger supports x-nullable to allow Schema properties or request parameters to be nullable:

	
last_modified(:string, "Datetime of video file last modification",
  format: "date-time",
  "x-nullable": true
)
	

Tip 2

Avoid using pair of actual type and :null atom to express that the Schema property or Request parameter is nullable. Use x-nullable mentioned in Tip 1 instead. Fields defined like that will produce errors when you validate your swagger spec file inside swagger editor.

	
      
# Don't do this
last_modified([:string, :null], "Datetime of video file last modification",
  format: "date-time"
)
	

You can follow me on Twitter if you liked this article: @vladimirgorej

 

Generate API Documentation Effortlessly from your Phoenix/Elixir code series:

  • Part 1: Phoenix Swagger
  • Part 2: Bureaucrat - API Documentation from tests
  • Part 3: Slate - Beautiful static HTML documentation