Skip to content

cogna-dev/mapi

Repository files navigation

mapi

 ███╗   ███╗ █████╗ ██████╗ ██╗
 ████╗ ████║██╔══██╗██╔══██╗██║
 ██╔████╔██║███████║██████╔╝██║
 ██║╚██╔╝██║██╔══██║██╔═══╝ ██║
 ██║ ╚═╝ ██║██║  ██║██║     ██║
 ╚═╝     ╚═╝╚═╝  ╚═╝╚═╝     ╚═╝

OpenAPI → MoonBit. Type-safe server stubs, generated.

CI mooncakes License MoonBit


What is mapi?

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.


Features

  • 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 generate runs; only files carrying the // Code generated by mapi. DO NOT EDIT. header are updated
  • Spec drift detectionmapi diff compares your current spec against the saved .mapi/state.json and 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 primitivesHttpMethod, RequestEnvelope, ResponseEnvelope, and a precise typed error hierarchy (AppError, DecodeError) mean routing mistakes are caught at compile time, not at runtime
  • In-memory test hostInMemoryHost lets you drive your full routing and handler stack in unit tests without spinning up a real server or making network calls
  • Five focused CLI commandsinit, generate, check, diff, doctor — each doing exactly one thing, composable in scripts and CI pipelines

Installation

moon add cogna-dev/mapi

Or add to your moon.mod.json directly:

{
  "deps": {
    "cogna-dev/mapi": "0.1.1"
  }
}

CLI Usage

mapi init — scaffold a new project

Bootstrap a complete MoonBit HTTP project from an OpenAPI spec in one command.

mapi init --spec openapi.json --out ./my-api

Creates 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

mapi generate — regenerate the generated layer

After updating your OpenAPI spec, regenerate the typed scaffolding without touching your handlers.

mapi generate --spec openapi.json --project ./my-api

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


mapi check — verify all handlers are implemented

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.


mapi diff — compare spec against saved state

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.


mapi doctor — diagnose your environment

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.


Runtime Library

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.

Core types

// 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)
}

Building an App

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)

Testing with InMemoryHost

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)

Generated Typed HTTP Client

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.


Architecture

┌─────────────────────────────────────────────────────┐
│  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.


Benchmarks

Throughput comparison across three Petstore implementations (100 VUs, 30 s, 4 endpoints per iteration).

The mbt-mapi benchmark server is now a standalone MoonBit project under benchmarks/servers/mbt-mapi/, built with moon build . --target native --release. It depends on cogna-dev/mapi via a local path dependency, serves requests through moonbitlang/async/http, and routes every request through the real App::serve() pipeline while keeping benchmark-only state local to that server project.

Hardware

Item Detail
CPU Apple M5
Memory 32 GB
OS macOS 26.3 (darwin/arm64)

Results

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 %

RPS comparison

  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/


Development

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

License

Apache 2.0

About

Web framework for MoonBit

Resources

License

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors