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/
oaspec ships in two flavors:
- Library API (the runtime contract for generated clients, plus the
generator itself for in-process use) — install via
gleam addfrom Hex. - CLI (the
oaspecbinary that drivesinit/generate/validateon 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_jsongleam_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.
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.
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# 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.yamloaspec 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.
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.
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.
gleam add oaspec_httpcimport 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.
gleam add oaspec_fetchimport 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.
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.
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.
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.
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.
- 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
- doc/openapi-support.md — what
oaspecsupports, what it rejects, and mode-specific feature restrictions - doc/configuration.md —
oaspec.yamlfields, CLI flags, multi-target codegen, and thevalidatemode - doc/server-adapters.md —
mist/wispadapter recipes for the generated router - doc/server-security.md — server-side auth enforcement model and patterns
- doc/library-api.md — using
oaspecas a Gleam library (parse → generate pipeline) - examples/ — runnable examples covered by CI
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 |