Skip to content

nao1215/oaspec

oaspec

Hex package HexDocs License Quick Tests Examples Adapters

Generate Gleam client and server modules from OpenAPI 3.x specs.

OpenAPI in → typed Gleam client and server out, with no per-operation glue code to write or maintain. The generator owns request and response types, encoders, decoders, validation guards, and the router; you wire credentials and a transport adapter, then call typed operation functions.

import api/client
import oaspec/httpc
import oaspec/transport

let send =
  httpc.send
  |> transport.with_base_url(client.default_base_url())

let assert Ok(pets) = client.list_pets(send, limit: Some(10), offset: None)

API reference: https://hexdocs.pm/oaspec/

Install

oaspec ships in two flavors:

  • Library API (the runtime contract for generated clients, plus the generator itself for in-process use) — install via gleam add from Hex.
  • CLI (the oaspec binary that drives init / generate / validate on the command line) — install from a GitHub release or build from source.

Most users want both: gleam add oaspec gleam_json in the project that consumes the generated code, and the CLI installed system-wide to run oaspec generate.

gleam add oaspec gleam_json

gleam_json is added in the same step because the generated decode.gleam, encode.gleam, guards.gleam, and router.gleam modules import gleam/json directly. Without it as a direct dependency, gleam check warns on every generated file.

CLI (GitHub release)

Requires Erlang/OTP 27+. The release artifact is an Erlang escript, so the same binary runs anywhere Erlang is available.

curl -fSL -o oaspec https://github.com/nao1215/oaspec/releases/latest/download/oaspec
chmod +x oaspec
sudo mv oaspec /usr/local/bin/

On Windows, download oaspec from the latest release and run it with escript oaspec <command>. Erlang/OTP 27+ must be on your PATH.

CLI (build from source)

Requires Gleam 1.15+, Erlang/OTP 27+, and rebar3.

git clone https://github.com/nao1215/oaspec.git
cd oaspec
gleam deps download
gleam run -m gleescript
sudo mv oaspec /usr/local/bin/   # or anywhere on PATH

Quickstart

# 1. (Skip if you have your own.) Fetch a sample spec.
curl -fSL -o openapi.yaml https://raw.githubusercontent.com/nao1215/oaspec/main/test/fixtures/petstore.yaml

# 2. Create oaspec.yaml — uncomment `input:` and point it at your spec.
oaspec init

# 3. Generate.
oaspec generate --config=oaspec.yaml

oaspec init writes a fully-commented template; package: api is the only uncommented field. All path-valued config fields (input, output.dir, output.server, output.client) are resolved relative to the current working directory when oaspec runs, not the config file location. See doc/configuration.md for the full set of fields, CLI flags, multi-target codegen, and validate mode.

Generated files

Given one OpenAPI spec, oaspec writes modules you can keep in your repository:

src/my_api/                  # server (mode: server | both)
  types.gleam
  decode.gleam
  encode.gleam
  request_types.gleam
  response_types.gleam
  guards.gleam
  handlers.gleam             # user-owned, written once, never overwritten
  handlers_generated.gleam
  router.gleam

src/my_api_client/           # client (mode: client | both)
  types.gleam
  decode.gleam
  encode.gleam
  request_types.gleam
  response_types.gleam
  guards.gleam
  client.gleam

The default base directory is ./src so the generated modules drop straight into the standard Gleam project layout — gleam build picks them up immediately. Override via output.dir (or per-target output.server / output.client) in oaspec.yaml if you want them elsewhere.

handlers.gleam is the one user-owned file — the generator writes panic stubs on the first run and skips it afterwards, so your implementations survive regeneration. Everything else is regenerated as the spec changes.

Using the generated client

Generated clients depend on a tiny pure runtime (oaspec/transport) instead of any specific HTTP library. Operations expose both synchronous transport.Send entry points and asynchronous transport.AsyncSend variants, so the same generated code runs against real HTTP, fakes, or any future runtime.

Adapters that bridge transport.Send / transport.AsyncSend to a real runtime live as sibling Gleam packages under adapters/, so the root oaspec package never depends on gleam_httpc or any specific HTTP runtime.

BEAM (oaspec_httpc)

gleam add oaspec_httpc
import api/client
import oaspec/httpc
import oaspec/transport

let send =
  httpc.send
  |> transport.with_base_url(client.default_base_url())

let result = client.list_pets(send, limit: Some(10), offset: None)

Runnable example: examples/petstore_client. Run it with just example-petstore.

JavaScript (oaspec_fetch)

gleam add oaspec_fetch
import api/client
import oaspec/fetch
import oaspec/transport

let send =
  fetch.send
  |> transport.with_base_url(client.default_base_url())

client.list_pets_async(send, limit: Some(10), offset: None)
|> transport.run(fn(result) {
  let _ = result
  Nil
})

Runnable example: examples/petstore_client_fetch. Run it with just example-petstore-fetch.

Tests (oaspec/mock)

import oaspec/mock

let send = mock.text(200, "[{\"id\": 1, \"name\": \"Fido\"}]")
let assert Ok(_) = client.list_pets(send, limit: None, offset: None)

oaspec/mock is a pure in-memory transport — no network, no FFI — so generated clients can be exercised in gleam test without any HTTP adapter. The petstore example above is built on it.

Authenticated requests

oaspec/transport ships middleware for base URL override, default headers, and OpenAPI security. with_security walks the request's declared OR-of-AND alternatives and applies the first one whose required schemes have credentials. The same with_* middleware works for both transport.Send and transport.AsyncSend.

let send =
  httpc.send
  |> transport.with_base_url(client.default_base_url())
  |> transport.with_security(
    transport.credentials()
    |> transport.with_bearer_token("BearerAuth", token),
  )

Each operation also exposes build_<op>_request and decode_<op>_response helpers, plus request-object wrappers for both sync and async call paths, so callers can drive the request and response halves independently — useful for retry middleware, logging, or testing decoding in isolation.

Adapter availability

Both adapters are published to Hex from this repository on tag push: oaspec_httpc-v* for the BEAM adapter, oaspec_fetch-v* for the JavaScript one. If gleam add oaspec_httpc reports package not found, no adapter release has been cut yet — depend on the adapter via a path dependency to a local checkout of oaspec until the first tag push. Pure git = "..." dependencies do not work in that interim state because each adapter lives in a subdirectory of the repo and Gleam's gleam.toml parser does not support a subpath field on git dependencies as of Gleam 1.16. See examples/petstore_client_fetch/gleam.toml for the canonical path-dependency layout.

Using the generated server

The codegen emits a single pure router function, and adapters bridge it to a real HTTP framework. api/router.route/6 takes the primitive pieces of a request — state, method, path, query, headers, body — and returns a ServerResponse whose body is a sum (TextBody(String), BytesBody(BitArray), EmptyBody), so binary endpoints carry real bytes through without a String round-trip:

import api/handlers
import api/router

let state = handlers.State
let response = router.route(state, "GET", ["pets"], dict.new(), dict.new(), "")

case response.body {
  router.TextBody(text) -> ...
  router.BytesBody(bytes) -> ...
  router.EmptyBody -> ...
}

Because the router is pure and synchronous, it is also trivial to test in isolation without an HTTP server. Runnable framework-free example: examples/server_adapter. Run it with just example-server-adapter.

For mist and wisp recipes that decompose the framework's request into the six primitives and render the ServerResponse back, see doc/server-adapters.md.

The generated router parses but does not enforce security: declarations on operations — handlers must check Authorization / X-Api-Key / cookies themselves and return their own 401. See doc/server-security.md for the rationale and two enforcement patterns.

Best for

  • Generating typed Gleam clients from an OpenAPI contract
  • Keeping request and response types in sync with an external API spec
  • Bootstrapping server-side types, handlers, and router support from the same source spec
  • Catching unsupported spec features early in CI instead of after code generation

Documentation

Development

This project uses mise for tool versions and just as a task runner.

mise install
just check
just shellspec
just integration
Command Tool What it tests
just test gleeunit Parser, validator, naming, config, collision detection
just shellspec ShellSpec CLI behaviour, file generation, content, unsupported feature detection
just integration gleeunit Generated code compiles and the generated modules work together

License

MIT

About

Generate strongly typed Gleam server stubs and client SDK from OpenAPI 3.x specifications

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages