Under development — APIs may change, use main branch.
OpenAPI code generation for Gleam. Parse specs, generate types, routes, clients, middleware, and React Query/SWR hooks.
Uses taffy for YAML parsing.
| Feature | Status |
|---|---|
| Parse OpenAPI 3.0/3.1/3.2 (YAML + JSON) | Working |
| $ref resolution across files | Working |
| Spec bundling (like redocly bundle) | Working |
| Generate Gleam types + decoders + encoders | Working |
| Generate Gleam route matching | Working |
| Generate Gleam middleware (auth, CORS) | Working |
| Generate TypeScript types | Working |
| Generate TypeScript fetch client | Working |
| Generate React Query hooks | Working |
| Generate SWR hooks | Working |
| Customizable templates (handles) | Working |
| Config file support | Working |
| Schema validation constraints in decoder | Incomplete |
| Extension fields (x-*) parsing | Not yet |
| Generated Gleam client imports | Bug |
| Zod/Valibot validation generation | Planned |
# Add to your gleam.toml
# [dependencies]
# nori = { git = "https://github.com/qwexvf/nori", ref = "main" }
# taffy = { git = "https://github.com/qwexvf/taffy", ref = "main" }
# Initialize (creates config, templates, starter spec)
gleam run -m nori/cli -- init
# Edit openapi.yaml with your spec, then generate
gleam run -m nori/cli -- generateFrom an OpenAPI spec, nori generates:
Gleam (server-side):
types.gleam— Record types, decoders (gleam/dynamic/decode), JSON encodersroutes.gleam—Routeunion type +match_route(method, segments)pattern matchermiddleware.gleam— Auth extractors, CORS, content-type validation
TypeScript (client-side):
types.generated.ts— Interfaces/types from schemasclient.generated.ts— Typedfetch()wrapper per endpointhooks.generated.ts— React QueryuseQuery/useMutationhooksswr-hooks.generated.ts— SWR hooks
import wisp.{type Request, type Response}
import generated/routes
import generated/types
pub fn handle_request(req: Request) -> Response {
let segments = wisp.path_segments(req)
case routes.match_route(req.method, segments) {
routes.ListTodos -> {
let items = get_todos_from_db()
let body = json.array(items, types.encode_todo)
json_response(body, 200)
}
routes.GetTodo(id) -> {
// ...
}
routes.NotFound -> wisp.not_found()
}
}See examples/wisp_app/ for a complete working example.
gleam run -m nori/cli -- init # Scaffold project
gleam run -m nori/cli -- generate # Generate from config
gleam run -m nori/cli -- generate --spec=./api.yaml # Generate from spec
gleam run -m nori/cli -- bundle spec.yaml # Bundle split specs
gleam run -m nori/cli -- validate spec.yaml # Validate spec# nori.config.yaml
spec: ./openapi.yaml
output:
gleam:
enabled: true
dir: ./src/generated
generated_suffix: false # types.gleam (not types.generated.gleam)
typescript:
enabled: true
dir: ./src/api
generated_suffix: true # types.generated.ts
use_interfaces: true
use_exports: true
react_query:
enabled: true
dir: ./src/api
swr:
enabled: falseSee nori.config.example.yaml for all options with documentation.
TypeScript generation uses handles templates. Run nori init to get editable .hbs files in templates/:
templates/
├── typescript_types.hbs # Edit to customize TS type output
├── typescript_client.hbs # Edit to customize fetch client
├── typescript_react_query.hbs # Edit to customize React Query hooks
└── typescript_swr.hbs # Edit to customize SWR hooks
examples/petstore/— Generated output from Petstore specexamples/realworld/— Generated output from a blog API (users, posts, comments, enums, allOf)examples/wisp_app/— Working Todo API server with Wisp
gleam test # Run tests (74 tests)
gleam check # Type checkimplemented with help of claude code