Documenting APIs: Implementing OpenAPI
APIs are contracts that stipulate expectations, responsibilities, and mechanisms for identifying faults as services evolve. To ensure these contracts are effectively implemented, the supporting documentation must be current, precise, and easily navigable.
Endpoint Definitions
- Endpoints: Clearly describe each endpoint, including its purpose, expected inputs, and outputs.
- Parameters and Request Bodies: Detail all parameters (path, query, header, or cookie) and request bodies, specifying their structure and type.
Data Handling Specifications
- Data Formats: Specify the formats for request and response payloads to ensure accurate data transmission.
- Responses: Outline possible responses for each operation, detailing the response content and associated status codes.
Operational Protocols
- Protocols: Document the communication protocols, such as HTTP or HTTPS, including methods of securing data during transmission and any authentication requirements.
- Security Schemes: Define and describe authentication methods and security requirements to ensure secure API access.
Reusable Components and Organization
- Components: Document components such as responses, parameters, request bodies, headers, and examples for consistency across the API.
- Tags: Organize and categorize operations by resources or other qualifiers, improving navigability and readability.
OpenAPI Specification (OAS)
The OpenAPI Specification (OAS) offers a comprehensive framework for documenting RESTful APIs, enhancing both accessibility and efficiency.
Abstraction
OpenAPI ensures developers can implement APIs without having access to the source code or needing to know the implementation details, including the programming languages used. This facilitates easier integration across various technology stacks, promoting broader usability and adoption.
Consistent Framework Implementation
OpenAPI uses a consistent, structured format that is crucial for compatibility with tools like Swagger UI. This uniformity ensures that documentation can be automatically parsed and utilized by various interactive tools, enhancing the development process by allowing direct testing and interaction within the documentation itself.
Integrating OpenAPI in Elixir
Add Dependencies
Start by adding dependencies in the mix.exs
file.
defp deps do
[
{:open_api_spex, "~> 3.10"},
{:ymlr, "~> 2.0"},
{:poison, "~> 6.0"},
{:cors_plug, "~> 3.0"}
]
end
- open_api_spex: Leverages the OpenAPI Specification 3 to document, test, validate, and explore Phoenix APIs.
- ymlr: A YAML encoder for Elixir.
- poison: An Elixir JSON library.
- cors_plug: An Elixir Plug that adds Cross-Origin Resource Sharing (CORS).
While a more sophisticated CORS setup might be required for production environments or specific client needs, starting with cors_plug
provides a basic setup allowing you to make curl
calls to your local environment, which is useful for testing and development purposes.
Remember to update your dependencies.
Controllers
The open_api_spex
library generates OpenAPI documentation from Elixir code. It parses through routes, looking for operation
functions, allowing for the documentation code to reside alongside the actual API code.
operation(:index,
summary: "List superheroes",
responses: [
ok: {"Superheroes", "application/json", SuperheroesResponse}
]
)
def index(conn, _params) do
superheroes = Superheroes.list_superheroes()
render(conn, :index, superheroes: superheroes)
end
The snippet above is from the SuperheroController
, where the index
function renders a list of superhero resources. The operation
function identifies the :index
action and adds documentation, including a human-readable summary along with the expected response.
Separation of Concerns
Unlike Python’s FastAPI or Spring Boot’s Springfox, open_api_spex
separates the operation
function from the index
logic. This decoupling risks documentation drift as the API evolves. However, the BEAM community prioritizes explicitness over implicitness, and from this perspective, keeping them apart reduces the likelihood of unexpected application behaviors.
Schemes
open_api_spex
includes functionality to generate OpenAPI schemas in Elixir.
defmodule GameAppWeb.Schemas do
require OpenApiSpex
alias OpenApiSpex.Schema
defmodule Superhero do
OpenApiSpex.schema(%{
title: "Superhero",
description: "A superhero in the app",
type: :object,
properties: %{
id: %Schema{type: :integer, description: "Superhero ID"},
name: %Schema{type: :string, description: "Superhero name"},
location: %Schema{type: :string, description: "Superhero location"},
power: %Schema{
type: :integer,
description: "Superhero power level",
minimum: 1,
maximum: 100,
example: 75
}
},
required: [:name, :location, :power],
example: %{
"id" => 1,
"name" => "Superman",
"location" => "Metropolis",
"power" => 75
}
})
end
end
This schema specifies the shape and property types of a superhero, complete with human-readable descriptions and examples for each field. It effectively outlines the required and optional properties, ensuring that the API’s documentation is comprehensive and clear.
In this application, superhero attributes are also managed using Ecto schemas and changesets, which handle data validation and persistence. This introduces additional potential for documentation drift, where updates to the application logic might not be reflected in the OpenAPI documentation.
Parameter Validation
open_api_spex
enables behavior-level validation for incoming API parameters, ensuring adherence to predefined schemas. However, as superheroes are already validated through Ecto changesets, incorporating additional validation with open_api_spex
would needlessly complicate the validation logic.
Example Generation
open_api_spex
can generate data examples based on API schemas. However, for superheroes, we are using a factory, so this is not needed.
Generating a YAML File for OpenAPI Specifications
OpenAPI specifications are designed to be portable, allowing them to be easily shared or imported into various tools for viewing, exploring, or generating code. This portability facilitates collaboration and integration across different platforms and tools.
To export the OpenAPI specification to a YAML file in an Elixir project using open_api_spex
, follow these steps:
-
Create an
open_api_spec
Module: Define a module that constructs the OpenAPI specification.defmodule GameAppWeb.ApiSpec do @behaviour OpenApiSpex.OpenApi alias OpenApiSpex.{Info, OpenApi, Paths} alias GameAppWeb.Router @impl OpenApi def spec do %OpenApi{ servers: [ %OpenApiSpex.Server{url: "http://localhost:4080"} ], info: %Info{ title: "Superheroes API", version: "1.0" }, paths: Paths.from_router(Router) } |> OpenApiSpex.resolve_schema_modules() end end
This module gathers all necessary API information and configurations, resolving schema modules to ensure all components of the API are correctly detailed in the specification.
-
Add Logic to the Makefile:
generate.openapi: @echo "Generating OpenAPI documentation..." MIX_ENV=dev mix openapi.spec.yaml --spec GameAppWeb.ApiSpec
Integrating Swagger UI
Swagger UI is a popular tool for visualizing and interacting with API documentation. The open_api_spex
library simplifies the integration of Swagger UI into Phoenix applications with a few lines of code in the router.
defmodule GameAppWeb.Router do
use GameAppWeb, :router
@swagger_ui_config [
path: "/api/openapi",
default_model_expand_depth: 3,
display_operation_id: true,
csp_nonce_assign_key: %{script: :script_src_nonce, style: :style_src_nonce}
]
pipeline :api do
plug :accepts, ["json"]
plug OpenApiSpex.Plug.PutApiSpec, module: GameAppWeb.ApiSpec
end
scope "/api" do
pipe_through :api
resources "/superheroes", GameAppWeb.SuperheroController, except: [:new, :edit]
get "/openapi", OpenApiSpex.Plug.RenderSpec, []
end
scope "/" do
pipe_through :browser
get "/docs", OpenApiSpex.Plug.SwaggerUI, @swagger_ui_config
end
end
By adding this configuration to the GameAppWeb.Router
, the application provides a Swagger UI at /docs
endpoint. Now, by visiting http://127.0.0.1:4080/docs, developers and stakeholders can easily explore and test the API functionalities hosted locally using Swagger UI.