Packages
oaspec
0.58.1
0.68.0
0.67.0
0.66.0
0.65.0
0.64.0
0.63.0
0.62.0
0.61.0
0.60.0
0.59.0
0.58.1
0.58.0
0.57.0
0.56.0
0.55.0
0.54.0
0.53.0
0.52.0
0.51.0
0.50.0
0.49.0
0.48.0
0.47.0
0.46.0
0.45.0
0.44.0
0.43.0
0.42.0
0.41.0
0.40.0
0.39.0
0.38.0
0.37.0
0.36.0
0.35.0
0.34.0
0.33.0
0.32.0
0.31.0
0.30.0
0.29.0
0.28.0
0.27.0
0.26.0
0.25.0
0.24.0
0.23.0
0.22.0
0.21.0
0.20.0
0.19.0
0.18.0
0.17.0
0.16.0
0.15.0
0.14.0
0.13.0
0.12.0
0.11.0
0.10.0
0.9.0
0.8.0
0.7.0
0.6.3
0.6.1
0.6.0
0.5.0
0.4.0
0.3.0
0.1.3
Generate Gleam code from OpenAPI 3.x specifications
Current section
Files
Jump to
Current section
Files
README.md
# oaspec
[](https://hex.pm/packages/oaspec)
[](https://hexdocs.pm/oaspec/)
[](https://github.com/nao1215/oaspec/blob/main/LICENSE)
[](https://github.com/nao1215/oaspec/actions/workflows/ci-quick.yml)
[](https://github.com/nao1215/oaspec/actions/workflows/ci-tests.yml)
[](https://github.com/nao1215/oaspec/actions/workflows/ci-examples.yml)
[](https://github.com/nao1215/oaspec/actions/workflows/ci-adapters.yml)
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.
```gleam
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`.
```sh
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.
```sh
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](https://github.com/nao1215/oaspec/releases/latest) 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`.
```sh
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
```sh
# 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](./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:
```text
gen/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
gen/my_api_client/ # client (mode: client | both)
types.gleam
decode.gleam
encode.gleam
request_types.gleam
response_types.gleam
guards.gleam
client.gleam
```
`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/`](./adapters),
so the root `oaspec` package never depends on `gleam_httpc` or any
specific HTTP runtime.
### BEAM (`oaspec_httpc`)
```sh
gleam add oaspec_httpc
```
```gleam
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`](./examples/petstore_client).
Run it with `just example-petstore`.
### JavaScript (`oaspec_fetch`)
```sh
gleam add oaspec_fetch
```
```gleam
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`](./examples/petstore_client_fetch).
Run it with `just example-petstore-fetch`.
### Tests (`oaspec/mock`)
```gleam
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`.
```gleam
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`](./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:
```gleam
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`](./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](./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](./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
- [doc/openapi-support.md](./doc/openapi-support.md) — what `oaspec`
supports, what it rejects, and mode-specific feature restrictions
- [doc/configuration.md](./doc/configuration.md) — `oaspec.yaml` fields,
CLI flags, multi-target codegen, and the `validate` mode
- [doc/server-adapters.md](./doc/server-adapters.md) — `mist` / `wisp`
adapter recipes for the generated router
- [doc/server-security.md](./doc/server-security.md) — server-side auth
enforcement model and patterns
- [doc/library-api.md](./doc/library-api.md) — using `oaspec` as a Gleam
library (parse → generate pipeline)
- [examples/](./examples) — runnable examples covered by CI
## Development
This project uses [mise](https://mise.jdx.dev/) for tool versions and
[just](https://just.systems/) as a task runner.
```sh
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](LICENSE)