nori

Package Version Hex Docs

A foundation for working with OpenAPI specifications in Gleam. Parses OpenAPI 3.x into a typed Document IR, validates structure, surfaces unsupported capabilities, and exposes a language-agnostic codegen IR that built-in and third-party generators consume.

Bundled generators emit Gleam (types, routes, HTTP client, Wisp middleware) and TypeScript (types, fetch client, React Query, SWR). The codegen IR is a public contract — extension packages can plug in their own targets without forking nori.

α release — public APIs may shift before 1.0. Generated code compiles cleanly and is tested end-to-end against real Gleam projects.

Uses taffy for YAML parsing.

Capabilities

FoundationStatus
Parse OpenAPI 3.0 / 3.1 (YAML + JSON)Working
$ref resolution across filesWorking
Spec bundling (multi-file → single, like redocly bundle)Working
Spec validationWorking
Capability checking (fail-fast on unsupported features)Working
Public CodegenIR for third-party generatorsWorking
Built-in generatorsStatus
Gleam types + JSON decoders + encodersWorking
Gleam route matchingWorking
Gleam HTTP request buildersWorking
Gleam Wisp middleware (auth, CORS, content-type)Working
TypeScript typesWorking
TypeScript fetch client (cookie-auth aware)Working
React Query hooksWorking
SWR hooksWorking
Handlebars templates for TS customizationWorking
Known unsupported (will fail capability check)
discriminator polymorphismTracked
Callbacks / webhooks codegenTracked
Parameter styles deepObject / pipeDelimited / spaceDelimitedTracked
multipart/form-data / x-www-form-urlencoded request bodiesTracked
Roadmap
Schema validation constraints in decoder#3
Zod / Valibot validation generation#7

Install

gleam add nori

Quick start (CLI)

# 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 -- generate

Library API

Use nori as a library to parse, inspect, or build your own codegen on top of the CodegenIR contract.

import gleam/io
import gleam/list
import nori
import nori/capability

pub fn main() {
  let assert Ok(doc) = nori.parse_file("./openapi.yaml")

  // Surface anything the codegen can't handle, before generating.
  case nori.check_capabilities(doc) {
    Ok(_) -> Nil
    Error(issues) ->
      list.each(issues, fn(i) { io.println(capability.issue_to_string(i)) })
  }

  // The codegen IR is a stable public contract — drive your own generator.
  let codegen_ir = nori.build_ir(doc)
  io.println("Endpoints: " <> int.to_string(list.length(codegen_ir.endpoints)))
}

What it generates

From an OpenAPI spec, nori generates:

Gleam (server-side):

TypeScript (client-side):

Usage with Wisp

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.

CLI

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 -- generate --allow-unsupported          # Skip capability check
gleam run -m nori/cli -- bundle spec.yaml                      # Bundle split specs
gleam run -m nori/cli -- validate spec.yaml                    # Validate + capability check

By default generate aborts when the spec uses something nori can’t generate correctly (discriminators, callbacks, multipart bodies, deepObject params, etc.). Pass --allow-unsupported to proceed anyway with degraded output.

Config

# 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: false

See nori.config.example.yaml for all options with documentation.

Custom templates

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

Extending nori

The CodegenIR type in nori/codegen/ir is the public contract for generators. To add a new target (another language, framework, or tooling), build a satellite package that consumes it:

import nori
import nori/codegen/ir

pub fn generate(ir: ir.CodegenIR) -> String {
  // walk ir.types, ir.endpoints, ir.security_schemes, …
  // produce your own code as a string.
}

Planned satellite packages: nori_oauth (OAuth2 / OpenID Connect codegen), nori_multipart (multipart/form-data), nori_react_query (extracted from the bundled React Query generator). The bundled generators ship in nori core for convenience.

Development

gleam test    # Run tests (88 tests)
gleam check   # Type check

License

Apache-2.0. See LICENSE.

Search Document