Herve37

Herve37

For Elixir teams building APIs, what are you using for OpenAPI documentation and API lifecycle management?

We’re evaluating alternatives to Stoplight and looking for feedback on:

  • OpenAPI support

  • Documentation workflows

  • Testing capabilities

  • Migration experience

Any recommendations from teams running APIs in production?

Most Liked

andreashasse

andreashasse

Hi, I’m the author of phoenix_spectral | Hex which helps you keep openAPI specs, elixir types and conversion from json to internal data structures in sync. It and its sibling libraries are used at a set of production systems and the experience have been good so far. Happy to get feedback!

zoi | Hex is also worth looking into, but I’m not to familiar with that library.

lud

lud

Hello,

For all my new production work, I use oaskit (short for Open API Specification Kit), which I wrote.

On the client side, we use Orval, and to publish the APIs and link to other specifications for our apps and docs, we use Backstage (the UI is not great, but it works well).

If you want to look into oaskit, here is a simple example:

The spec module

Like with many other libraries, you start by defining a spec. Here, we’re pulling the routes from Phoenix controllers.

defmodule MyAppWeb.ApiSpec do
  use Oaskit
  alias Oaskit.Spec.Paths

  @impl true
  def spec do
    %{
      openapi: "3.1.1",
      info: %{title: "My App API", version: "1.0.0"},
      # Paths and schemas are collected from the operations declared in your
      # controllers, for every route matched by the filter.
      paths: Paths.from_router(MyAppWeb.Router, filter: &String.starts_with?(&1.path, "/api/"))
    }
  end
end

Router

In the router, besides your normal controller routes, you add the validation layer as a plug; otherwise, the OpenAPI doc is only declarative. With the plug, the library enforces validation of incoming requests.

You can also serve the OpenAPI spec itself, along with a UI page (Redoc) to explore it manually.

pipeline :api do
  plug :accepts, ["json"]
  plug Oaskit.Plugs.SpecProvider, spec: MyAppWeb.ApiSpec
end

scope "/api", MyAppWeb do
  pipe_through :api

  post "/users", UserController, :create

  # Serve the generated spec as JSON, plus a Redoc UI
  get "/openapi.json", Oaskit.SpecController, spec: MyAppWeb.ApiSpec
  get "/docs", Oaskit.SpecController, redoc: "/api/openapi.json"
end

Controller operations

In a controller, you can define schemas inline, though personally I’d group all schemas for a given domain in a single module.

# Oaskit docs help to wire validation to all controllers in the "web module"
use MyAppWeb, :controller
use JSV.Schema

# Request body
defschema CreateUser,
  name: string(minLength: 1),
  email: string(format: :email),
  age: optional(integer(minimum: 18, default: 18))

# Response body
defschema User,
  id: integer(),
  name: string(description: "The user's full name"),
  email: string()

operation :create,
  summary: "Create a user",
  # Schema modules are used here, but you can just use inline schemas like
  # %{type: :integer}
  request_body: CreateUser,
  # :created option here is for HTTP Error code 201, 200 would be [ok: User] or %{200 => User}
  responses: [created: User]

def create(conn, _params) do
  # With the validation layer, data is cast to a struct when using schema modules, but it's
  # totally optional. The raw data is always available.
  %CreateUser{name: name, email: email, age: age} = user_payload = body_params(conn)

  :ok = create_user(user_payload)

  user = %User{id: 1, name: name, email: email}

  conn
  |> put_status(:created)
  |> json(user)
end

Test

Testing was very important to me when designing the library. The tests use your OpenAPI definition module to know what to validate depending on the route or operation being called, so you know responses are correct with regard to what the spec declares. valid_response/3 asserts that the status, content type, and body all match the declared operation, and it returns the decoded body.

# To not state the spec module name in all tests, I generally define a wrapper like this in MyAppWeb.ConnCase
defp valid_response(conn, status) do
  Oaskit.Test.valid_response(MyAppWeb.ApiSpec, conn, status)
end

test "creates a user", %{conn: conn} do
  conn = post(conn, ~p"/api/users", %{name: "John", email: "john@example.com", age: 25})

  assert %{"id" => 1, "name" => "John"} = valid_response(conn, 201)
end

test "rejects invalid payloads", %{conn: conn} do
  # The ValidateRequest plug rejects this before the action runs
  conn = post(conn, ~p"/api/users", %{name: "", email: "nope"})

  assert json_response(conn, 422)
end

To publish to Backstage we just use the dump command from CI.

mix openapi.dump MyAppWeb.ApiSpec --pretty -o priv/openapi.json

There’s a Quickstart that walks through the full setup if you want to try it for real :slight_smile:

If you want to migrate and you already have an OpenAPI spec document you can use it with oaskit, though in that case oaskit will just bring validation to the table, since your spec already exists on its own.

arcanemachine

arcanemachine

My understanding is that there are 2 main choices for Elixir + OpenAPI specs:

  • Raw open_api_spex - Define specs manually. Flexible, but cumbersome

  • Ash Framework + ash_json_api - Derive specs from your existing resources. Automagic, but requires you to use Ash Framework (may not be for everyone), and also requires you to conform to JSON:API style specs.

Where Next?

Popular in Discussions Top

Jayshua
I recently came across the javascript library htmx. It reminded me a lot of liveview so I thought the community here might be interested....
New
owaisqayum
I have a sample string sentence = "Hello, world ... 123 *** ^%&*())^% %%:>" From this string, I want to only keep the integers, ...
New
thojanssens1
It would be nice to be able to define a redirect from one route to another from the router.ex file. E.g.: redirect "/", UserController, ...
New
AngeloChecked
What learn first? Rust or Elixir Hi Elixir community! I’m here because i want learn a new language. I’m a junior developer and mainly i ...
New
gausby
I asked this very same question on twitter and got some interesting feedback, but I thought it would be a good question to ask here as we...
1207 39523 209
New
AlexMcConnell
The reason that Rails is as popular as it is is because it’s very easy for relatively inexperienced developers to get a lot of work done....
588 19675 166
New
New
chulkilee
Here are the list of HTTP client libraries/wrappers, and some thoughts on HTTP client in general. I’d like to hear from others how they w...
New
pdgonzalez872
If this has been asked here before, please point me to where it was asked as I didn’t find it when I searched the forum. Maybe a mailing ...
New
Qqwy
Looking at the stacks that existing large companies have used, WhatsApp internally uses Mnesia to store the messages, while Discord uses ...
New

Other popular topics Top

axelson
This post is a wiki (feel free to hit the edit button near the bottom right of this post to add your own changes!) This post collects co...
239 48475 226
New
Darmani72
If I have a post route which an argument: post /my_post_route/:my_param1, MyController.my_post_handler How would get the post params ...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New
AngeloChecked
What learn first? Rust or Elixir Hi Elixir community! I’m here because i want learn a new language. I’m a junior developer and mainly i ...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
Qqwy
Update: How to use the Blogs & Podcasts section You can post links to your blog posts or podcasts either in one of the Official Blog...
3271 127536 1222
New
PeterCarter
There are pre-rolled solutions for other frameworks that do work. However, Phoenix does not seem to have these. Have people had good expe...
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New

We're in Beta

About us Mission Statement