███╗ ███╗ █████╗ ██████╗ ██╗
████╗ ████║██╔══██╗██╔══██╗██║
██╔████╔██║███████║██████╔╝██║
██║╚██╔╝██║██╔══██║██╔═══╝ ██║
██║ ╚═╝ ██║██║ ██║██║ ██║
╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝
OpenAPI → MoonBit. Type-safe server stubs, generated.
mapi is a CLI tool and runtime library for building HTTP servers in MoonBit. Point it at an OpenAPI 3.0 specification and it produces a fully typed, immediately compilable MoonBit project — complete with handler stubs, request decoders, route registration, and a host-adapter interface.
The contract is simple: you own the handlers; mapi owns the plumbing. Re-run generation after your spec evolves and your business logic stays untouched. The generated layer and your code live in separate packages, enforced by a DO NOT EDIT marker and a .mapi/state.json manifest that tracks exactly what was generated.
Whether you're scaffolding a greenfield service from an OpenAPI contract, evolving an existing API, or just want a battle-tested HTTP routing primitive for MoonBit, mapi gives you a clean, typed foundation to build on.
- Code generation from OpenAPI 3.0 — reads any valid OpenAPI 3.0 spec (JSON or YAML) and emits a complete, compilable MoonBit project with typed route registrations, request decoders, handler stubs, and a typed HTTP client
- Regeneration-safe — user-owned handler files are never overwritten on subsequent
generateruns; only files carrying the// Code generated by mapi. DO NOT EDIT.header are updated - Spec drift detection —
mapi diffcompares your current spec against the saved.mapi/state.jsonand surfaces added, removed, and unchanged routes before you regenerate - Host-adapter architecture — the runtime core (
lib/) is completely decoupled from I/O; any execution environment (Bun, Node, Deno, native Wasm) can be plugged in by implementing a thin host adapter - Type-safe HTTP primitives —
HttpMethod,RequestEnvelope,ResponseEnvelope, and a precise typed error hierarchy (AppError,DecodeError) mean routing mistakes are caught at compile time, not at runtime - In-memory test host —
InMemoryHostlets you drive your full routing and handler stack in unit tests without spinning up a real server or making network calls - Five focused CLI commands —
init,generate,check,diff,doctor— each doing exactly one thing, composable in scripts and CI pipelines
moon add cogna-dev/mapiOr add to your moon.mod.json directly:
{
"deps": {
"cogna-dev/mapi": "0.1.1"
}
}Bootstrap a complete MoonBit HTTP project from an OpenAPI spec in one command.
mapi init --spec openapi.json --out ./my-apiCreates a project structure with a clean separation between generated and user-owned code:
my-api/
├── moon.mod.json
├── src/
│ ├── generated/ # DO NOT EDIT — regenerated by mapi
│ │ ├── schemas.mbt # OpenAPI schema models
│ │ ├── operations.mbt # Typed operation inputs / outputs
│ │ ├── contract.mbt # Handler contract for server implementation
│ │ ├── router.mbt # Route registrations and server glue
│ │ ├── client.mbt # Typed HTTP client generated from spec
│ │ └── moon.pkg
│ └── handlers/ # Your code — never overwritten
│ ├── get_users.mbt
│ └── create_user.mbt
└── .mapi/
└── state.json # Spec snapshot used by diff / check
After updating your OpenAPI spec, regenerate the typed scaffolding without touching your handlers.
mapi generate --spec openapi.json --project ./my-apigenerate treats --project as an existing MoonBit package root: it reads the package name from moon.mod.json, regenerates src/generated/*, and leaves your existing module manifest untouched.
Only files with the // Code generated by mapi. DO NOT EDIT. header are modified. Your handlers are safe.
Confirms that every route declared in the current spec has a corresponding handler file in the project.
mapi check --project ./my-api
# check: all 12 handlers present ✓Useful as a CI gate to catch missing stubs before they reach production.
Shows what changed between your current spec and the .mapi/state.json snapshot — before you regenerate.
mapi diff --spec openapi.json --project ./my-api
# + createOrder (new handler stub will be created on next generate)
# - deleteWidget (orphaned handler — run generate to see warning)
# getUsers
# diff: +1 -1 =1+ = new routes, - = removed routes, unchanged routes are listed without a prefix.
Runs a series of pre-flight checks to verify your toolchain and project configuration are correct.
mapi doctor --project ./my-api --spec openapi.json
# [ok] moon CLI found
# [ok] moon.mod.json found in ./my-api
# [ok] .mapi/state.json found in ./my-api
# [ok] spec parsed successfully: openapi.json
# doctor: all checks passed ✓Run this first when something unexpected happens — it surfaces missing tools, corrupt state files, and unparseable specs before you spend time debugging.
Use cogna-dev/mapi/lib directly to build HTTP servers without any code generation. The library gives you a lightweight, typed routing core you can drive from any host environment.
// HTTP method enum — exhaustive, no stringly-typed methods
pub enum HttpMethod { Get | Post | Put | Patch | Delete | Head | Options }
// Envelopes carry the full request/response across the host boundary
pub struct RequestEnvelope { method : HttpMethod; path : String; body : String? }
pub struct ResponseEnvelope { status : Int; body : String }
// Typed error hierarchy — match on these instead of inspecting status codes
pub suberror AppError {
NotFound(String)
BadRequest(String)
Unauthorized
Forbidden
Internal(String)
}
pub suberror DecodeError {
MissingRequiredParam(String)
InvalidParamType(String, String)
InvalidBody(String)
}let app = App::new()
app.router.add(Get, "/users", fn(ctx) {
// ctx.request — the full RequestEnvelope
// ctx.params — path parameters extracted by the router
ResponseEnvelope::ok(body)
})
// Dispatch a request — call this from your host adapter
app.serve(request_envelope)InMemoryHost wires your App to a synchronous in-memory transport. No server, no ports, no flakiness.
let host = InMemoryHost::new(app)
// Drive any route directly
let resp = host.request(Get, "/users", None, None, None)
assert_eq!(resp.status, 200)
// Test error paths just as easily
let not_found = host.request(Get, "/users/99999", None, None, None)
assert_eq!(not_found.status, 404)Each generated project now includes src/generated/client.mbt, which gives you a static-typed API client derived directly from the same OpenAPI contract used for server scaffolding.
- One typed client method per operation (
ApiClient::list_pets, etc.) - Strongly typed request input (
ListPetsInput) and response output (ListPetsResponse) - Path/query/body request shaping generated from OpenAPI parameters and requestBody
- JSON response decoding into generated MoonBit types
- Deterministic status-code handling (preferred success status from OpenAPI, or fallback to any
2xx)
ApiClient is transport-agnostic: provide an ApiTransport with a send function that receives a RequestEnvelope and returns a ResponseEnvelope. This keeps the generated client reusable across Bun, Node, Deno, native hosts, and in-memory tests.
┌─────────────────────────────────────────────────────┐
│ mapi CLI (main/) │
│ init · generate · check · diff · doctor │
│ Reads OpenAPI spec → writes MoonBit source │
└──────────────────────┬──────────────────────────────┘
│ generates
▼
┌─────────────────────────────────────────────────────┐
│ Generated layer (src/generated/) │
│ schemas · operations · contract · router · client │
│ Typed server glue + typed HTTP client │
└──────────────────────┬──────────────────────────────┘
│ calls
▼
┌─────────────────────────────────────────────────────┐
│ Your handlers (src/handlers/) │
│ get_users.mbt · create_order.mbt · ... │
│ Business logic — never touched by regeneration │
└──────────────────────┬──────────────────────────────┘
│ served by
▼
┌─────────────────────────────────────────────────────┐
│ Runtime core (lib/) │
│ App · Router · HttpMethod · RequestEnvelope │
│ Pure MoonBit — no I/O, no runtime assumptions │
└──────────────────────┬──────────────────────────────┘
│ adapted by
▼
┌─────────────────────────────────────────────────────┐
│ Host adapter (e.g. servers/mbt-mapi/host.ts) │
│ Bun · Node · Deno · native Wasm · InMemoryHost │
│ Translates platform I/O ↔ mapi envelopes │
└─────────────────────────────────────────────────────┘
The runtime core has zero I/O dependencies. All platform interaction is delegated to a host adapter, which means the same MoonBit handler code can run under Bun today and a native Wasm runtime tomorrow without modification.
Throughput comparison across three Petstore implementations (100 VUs, 30 s, 4 endpoints per iteration).
The
mbt-mapibenchmark server is now a standalone MoonBit project underbenchmarks/servers/mbt-mapi/, built withmoon build . --target native --release. It depends oncogna-dev/mapivia a local path dependency, serves requests throughmoonbitlang/async/http, and routes every request through the realApp::serve()pipeline while keeping benchmark-only state local to that server project.
| Item | Detail |
|---|---|
| CPU | Apple M5 |
| Memory | 32 GB |
| OS | macOS 26.3 (darwin/arm64) |
| Server | RPS | avg (ms) | p50 (ms) | p90 (ms) | p95 (ms) | max (ms) | Checks |
|---|---|---|---|---|---|---|---|
| go-gin | 3 392 | 4.31 | 0.74 | 16.39 | 22.59 | 59.91 | 100 % |
| rust-axum | 3 213 | 6.01 | 1.52 | 19.03 | 25.67 | 94.23 | 100 % |
| mbt-mapi | 467 | 188.90 | 2.18 | 869.01 | 1 113.84 | 2 780.15 | 100 % |
go-gin ████████████████████████████████▏ 3 392
rust-axum ██████████████████████████████▎ 3 213
mbt-mapi ████▎ 467
└─────────┴─────────┴─────────┴──────── RPS
0 1000 2000 3000 3500
Go-gin leads on both throughput and latency, with rust-axum still close behind. The native mbt-mapi benchmark server now measures a standalone MoonBit benchmark project plus the moonbitlang/async/http host layer; under this workload its median stays low, but the tail latency is still much higher than the Go and Rust implementations.
Full methodology and raw JSON results:
benchmarks/
moon check # type-check the whole workspace
moon fmt # format all source files
moon build # compile
moon test # run tests
moon run main -- --help # run the CLI