Herve37

Herve37

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?

Showing Posts 1 to 10

Thereve

Thereve

OpenAPI import/export is usually the easy part — the bigger differences show up around keeping specs in sync, handling changes, generating docs, and making sure tests actually reflect the contract.

For migration, I’d definitely try moving a smaller API first. A lot of tools look similar on paper but differ quite a bit once you bring over existing schemas, examples, auth flows, and CI checks.

tomkonidas

tomkonidas

I have had good experience using open_api_spex | Hex for documentation.

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.

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.

phcurado

phcurado

Thanks for mentioning Zoi @andreashasse.

I don’t know much about spotlight or alternatives, I’m usually defining all of these things internally in my application so I will just show how I’m doing it.

For OpenAPI support, I create a module with all API definitions there and use Zoi.to_json_schema/1 (OpenAPI 3.1 is fully compatible with JsonSchema).

This is a simplified version of how I’m doing in some services:

defmodule MyApp.OpenAPI do
  def spec do
    %{
      openapi: "3.1.0",
      info: %{
        title: "MyApp API",
        version: "1.0.0"
      },
      paths: %{
        "/things" => %{
          post: %{
            summary: "Create a thing",
            operationId: "createThing",
            tags: ["Things"],
            requestBody: %{
              required: true,
              content: %{
                "application/json" => %{
                  schema: %{"$ref" => "#/components/schemas/CreateThing"}
                }
              }
            },
            responses: %{
              "201" => %{
                description: "Thing created",
                content: %{
                  "application/json" => %{
                    schema: %{"$ref" => "#/components/schemas/Thing"}
                  }
                }
              }
            }
          }
        }
      },
      components: %{
        schemas: %{
          "CreateThing" => Zoi.to_json_schema(create_thing()),
          "Thing" => Zoi.to_json_schema(thing())
        }
      }
    }
  end

  defp create_thing do
    Zoi.map(%{
      name: Zoi.string(),
      amount: Zoi.integer(coerce: true)
    })
  end

  defp thing do
    Zoi.map(%{
      id: Zoi.uuid(),
      name: Zoi.string(),
      amount: Zoi.integer()
    })
  end
end

And in Phoenix I expose it with a normal route/controller:

get "/openapi.json", OpenAPIController, :index

controller:

def index(conn, _params) do
  json(conn, MyApp.OpenAPI.spec())
end

This mimics how usually you would write the OpenAPI definitions by hand but using elixir code.

For documentation, I expose swagger docs using another module with the swagger HTML and referencing the openAPI endpoint above.

For testing capabilities, usually I use the Phoenix helpers for testing requests to the APIs:

test "creates a thing", %{conn: conn} do
  conn = post(conn, ~p"/things", %{name: "Some thing"})

  assert %{"id" => _id, "name" => "Some thing"} = json_response(conn, 201)
end

and in controllers you can make sure you are parsing exactly the shape your API is expecting:

with {:ok, attrs} <- Zoi.parse(MyApp.OpenAPI.create_thing(), params) do
  # ...
end

This would not replace spotlight tho, it’s quite handmade/internal solution but for me is very flexible and I can extend it from there.

phoenix_spectral looks great as well, I haven’t seem it before!

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.

tior

tior

We are also using Oaskit and are happy with it. We switched to Oaskit when we saw that it supported 3.1, unlike OpenAPISpex

Herve37

Herve37 OP

Agreed. The migration itself isn’t usually the hard part keeping specs, docs, and tests aligned over time is. That’s one of the reasons we’re evaluating tools rather than focusing only on import/export compatibility. Thanks for your reply :cowboy_hat_face:

Herve37

Herve37 OP

cool I will try it also

MrYawe

MrYawe

We also migrated from OpenAPISpex to oaskit and we are very happy with it.

Our fronted team use https://openapi-ts.dev and for documentation we use Scalar.

Where Next? Top

Trending in Discussions Top

AstonJ
As the title says, please share what you’ve been up to with Elixir. Whether that’s been learning it, looking into it, making stuff with i...
2977 94592 917
New
cblavier
Hey there, It’s been more than a year since we started using LiveView as our main UI library and building a whole library of UI componen...
New
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
heathen
Quite interesting article Google brought me. Didn’t find any mentions about it here. What do you think in general? Would you use togethe...
New
maennchen
:warning: Security advisory: Decimal DoS vulnerability A vulnerability has been published for decimal where very large exponents can cau...
New
marciol
It would be helpful to have a list of companies worldwide that hire engineers without prior experience in Elixir. Often, it can be quite ...
New
Null-logic-0
What IDE or editor are you using for Elixir development? Personally, I use Zed, and I really like it, but sometimes I wish there were a ...
New

Other Trending Topics Top

marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
webofbits
With AI doing more of the implementation work, I’ve been wondering how much coding I should deliberately keep doing myself. My main conc...
#ai
New
webofbits
Aludel - LLM Evaluation Workbench Aludel is an embeddable Phoenix LiveView dashboard for evaluating and comparing LLM prompts across mult...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews