moonapi
Examples

Run every feature. Twenty-six ways to build with moonapi.

Each example in src/examples/ is a small, runnable program. Below they are grouped by what they show — from a two-route hello world to a full service that uses every part of the framework. Open any card's source, or run it yourself:

$ moon run src/examples/02-routing --target native

Start here

Two small programs that show the shape of every moonapi service: routes in, response out, document on demand.

01-openapi

OpenAPI out of the box

Register a few routes, run a request in-process, then emit the OpenAPI 3.1 document the routes describe.

let app = @moonapi.App::new()
app.get("/", _ctx => @moonapi.text(200, "Hello from moonapi"))
app.get(
  "/users/{id}",
  ctx => @moonapi.text(200, "user " + ctx.param("id").unwrap()),
  summary="fetch a user",
)
app.post(
  "/users",
  _ctx => @moonapi.text(201, "created"),
  summary="create a user",
)
View source on GitHub →
02-routing

Routing, verbs and path parameters

Bind every HTTP verb, capture a {id} path parameter, and see the framework return 404 or 405 on its own.

let app = @moonapi.App::new()
app.get("/items/{id}", ctx => {
  @moonapi.text(200, "item " + ctx.param("id").unwrap())
})
app.post("/items", _ctx => @moonapi.text(201, "created"))
app.put("/items/{id}", _ctx => @moonapi.text(200, "replaced"))
app.patch("/items/{id}", _ctx => @moonapi.text(200, "patched"))
app.delete("/items/{id}", _ctx => @moonapi.text(200, "deleted"))
app.route(Head, "/items", _ctx => @moonapi.text(200, ""))
app.route(Options, "/items", _ctx => @moonapi.text(204, ""))
let hit = (verb : String, path : String) => {
  let req : @moonasgi.Request = {
View source on GitHub →

Bodies & validation

Turn a request into a typed, checked value — and decide exactly what the response reveals.

04-descriptors

Validate against a descriptor

An endpoint descriptor checks the request; a body that breaks it comes back with located errors.

let app = users_app()
let json_headers = [("content-type", "application/json")]
let hit = (verb : String, path : String, body : String) => {
  let req : @moonasgi.Request = {
    http_method: verb,
    path,
    query_string: b"",
    headers: json_headers,
    body: @utf8.encode(body),
  }
  let r = app.handle(req)
  println("\{verb} \{path} -> \{r.status} \{@utf8.decode_lossy(r.body[:])}")
View source on GitHub →
05-constraints

Field constraints

Constrain fields with patterns, ranges and shapes, then validate the whole request at the boundary.

let app = @moonapi.App::new()
app.post(
  "/orders",
  ctx => {
    let errs = ep.validate(ctx)
    if errs.length() > 0 {
      @moonapi.unprocessable(errs)
    } else {
      @moonapi.text(201, "order accepted")
    }
  },
  endpoint=ep,
View source on GitHub →
06-typed-body

Typed bodies, two ways

Validated extraction returns located errors; unchecked extraction returns a total option. Choose per route.

let app = @moonapi.App::new()
// Validated extraction: conform, or get located errors.
app.post("/greet", ctx => {
  match (ctx.body_validated(GreetReq::schema()) : Result[GreetReq, _]) {
    Ok(g) => @moonapi.text(200, "Hello, " + g.name)
    Err(errs) => @moonapi.unprocessable(errs)
  }
})
// Unchecked extraction: a total `T?`, `None` when the body cannot shape-match.
app.post("/echo", ctx => {
  let parsed : GreetReq? = ctx.body()
  match parsed {
View source on GitHub →
07-request-extractors

Typed request extractors

Declare a path, query, header and cookie parameter once; the endpoint validates and binds them for you.

let app = @moonapi.App::new()
let search = @moonapi.Endpoint::new(params=[
  @moonapi.Param::new("cat", InPath),
  @moonapi.Param::new("limit", InQuery, schema=SInt),
  @moonapi.Param::new("x-trace", InHeader),
  @moonapi.Param::new("session", InCookie),
])
app.get(
  "/search/{cat}",
  ctx => {
    let errs = search.validate(ctx)
    if errs.length() > 0 {
View source on GitHub →
08-response-model

Response models

Build a full internal record, then project only the safe fields through a response model.

let app = @moonapi.App::new()
app.get("/me", _ctx => {
  // The handler builds a full record; the model decides what leaves.
  let internal : Json = {
    let m : Map[String, Json] = Map([
      ("id", Json(233)),
      ("name", Json("lfanke")),
      ("password_hash", Json("deadbeef")),
      ("email", Json("lfanke@example.com")),
    ])
    Json(m)
  }
View source on GitHub →
19-json-schema

Inspect the emitted JSON schema

See exactly what schema a typed body contributes to the generated OpenAPI document.

let app = @moonapi.App::new()
app.post(
  "/posts",
  ctx => @moonapi.json(201, ctx.body_json().unwrap_or(Json::null())),
  endpoint=@moonapi.Endpoint::new(request_body=body),
)

// The document says exactly what was written.
let doc = app.openapi()
println(
  "schema: " +
  at(doc, ["components", "schemas", "Post", "properties"]).stringify(),
View source on GitHub →
26-described-model

Describe then validate

Attach a field-level description to a model and validate the request against it.

let app = @moonapi.App::new()
app.post(
  "/orders",
  ctx => {
    let errs = ep.validate(ctx)
    if errs.length() > 0 {
      @moonapi.unprocessable(errs)
    } else {
      @moonapi.text(201, "order accepted")
    }
  },
  endpoint=ep,
View source on GitHub →

Dependency injection

Share services across handlers without threading them through every call.

09-dependency-injection

A request-scoped container

Register services by name; the container builds, caches and tears them down once per request.

enum Dep {
  Cfg(String)
  Conn(String)
  Repo(String)
}

fn Dep::show(self : Dep) -> String {
  match self {
    Cfg(s) => s
    Conn(s) => s
    Repo(s) => s
  }
View source on GitHub →

Security

Declare how requests prove who they are, and attach the proof each route needs.

10-oauth2-jwt

OAuth2 password flow with JWT

Accept a password form, mint a signed access token, and return it in the standard token response.

let app = @moonapi.App::new()
app.post("/token", ctx => {
  match ctx.oauth2_password_form() {
    Some(form) =>
      if form.username == "lfanke" && form.password == "qwq" {
        let tok = @moonapi.create_access_token(
          form.username,
          secret,
          now,
          scopes=form.scopes,
        )
        @moonapi.token_response(tok)
View source on GitHub →
11-per-op-security

Per-operation security

Attach a security requirement to a route so the guard must admit the request before the handler runs.

let app = @moonapi.App::new()
app.set_clock(() => now)
app.secure_oauth2("OAuth2", bearer, scopes=[
  ("items:read", "read items"),
  ("items:write", "write items"),
])
// The handler is never reached unless the guard admits the request.
app.get(
  "/items",
  _ctx => @moonapi.text(200, "the item list"),
  summary="list items",
  security=[
View source on GitHub →
12-security-schemes

API keys, bearer, basic, OAuth2

Declare every scheme the API accepts and read a credential from each carrier in one route.

let app = @moonapi.App::new()
app.add_security_scheme("BearerJWT", HttpBearer(bearer_format="JWT"))
app.add_security_scheme("ApiKeyHeader", ApiKeyHeader(name="X-API-Key"))
app.add_security_scheme("ApiKeyQuery", ApiKeyQuery(name="api_key"))
app.add_security_scheme("ApiKeyCookie", ApiKeyCookie(name="session"))
app.add_security_scheme("Basic", HttpBasic)
let bearer = @moonapi.OAuth2PasswordBearer::new("/token", "heke1228")
app.add_security_scheme(
  "OAuth2",
  bearer.scheme(scopes=[("read", "read access"), ("write", "write access")]),
)
View source on GitHub →

Composition & middleware

Stack layers, mount sub-apps, and wire the lifecycle around a single application.

13-forms-files

Forms and file uploads

Parse multipart forms, read individual fields and files, and enforce upload size limits.

let app = @moonapi.App::new()
app.post("/upload", ctx => {
  // A title and three tags is the whole of this form, so more is not it.
  let form = match ctx.form(limits={ ..@moonmime.limits, parts: 4, }) {
    Some(f) => f
    None =>
      return @moonapi.text(
        @moonapi.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
        "too many form parts",
      )
  }
  let title = form.field("title").unwrap_or("-")
View source on GitHub →
14-background-tasks

Work after the response

A wrapping layer runs a task once the response body has been sent to the client.

let app = @moonapi.App::new()
app.layer(
  @mid.Wrap(inner => {
    async fn(scope, receive, send) {
      inner(scope, receive, event => {
        send(event)
        if event is HttpResponseBody(more_body=false, ..) {
          log.push("response sent")
        }
      })
    }
  }),
View source on GitHub →
15-mounting

Mount sub-applications

Build a small admin app and an API app, then mount them at their own prefixes under one root.

let admin = @moonapi.App::new()
admin.get(
  "/stats",
  _ctx => @moonapi.text(200, "admin stats"),
  summary="stats",
)

// The API sub-app.
let api = @moonapi.App::new()
api.get(
  "/items",
  _ctx => @moonapi.text(200, "the items"),
View source on GitHub →
16-middleware

CORS, gzip and custom middleware

Add CORS and gzip as layers and slip a custom middleware into the request/response chain.

let app = @moonapi.App::new()
app.layer(
  @mid.Cors::new(origins=["https://app.example.com"], credentials=true, expose=[
    "X-Total",
  ]),
)
app.layer(@mid.Gzip::new(minimum=100))
app.middleware((req, call_next) => {
  let resp = call_next(req)
  @moonasgi.Response::new(
    resp.status,
    [("x-handled-by", "moonapi"), ..resp.headers],
View source on GitHub →
23-lifespan

Startup and shutdown hooks

Run code when the server starts and stops, and share state across the lifespan.

let app = @moonapi.App::new()
app.on_startup(state => {
  state["db"] = "pool-1"
  log.push("connect db")
})
app.on_startup(_ => log.push("warm cache"))
app.on_shutdown(_ => log.push("close db"))
app.on_shutdown(_ => log.push("drop cache"))
app.get("/db", ctx => {
  @moonapi.json(200, ctx.state().get("db").unwrap_or(Json::null()))
})
let client = @testing.Client::new(app.to_asgi())
View source on GitHub →
24-files

Serve files and data URLs

Stream a file with an etag and turn an inline data URL into a served response.

let app = @moonapi.App::new()
let report = b"a quarterly report, ten times longer than it needs to be"
let tag = @conditional.hash(report[:], digest=@sha2.Hasher::new())
app.get("/report.txt", ctx => {
  ctx.serve(report, filename="report.txt", etag=tag)
})
// A client posts back what `canvas.toDataURL()` gave it; the server serves
// what is inside, under the media type the URL itself names.
app.post("/thumbnail", ctx => {
  match ctx.body_json() {
    Some(String(url)) =>
      match @moonapi.data_response(url, filename="thumb") {
View source on GitHub →
27-annotations

Annotation routes and routers

Write routes as annotations, group them in a router, and include the router under a prefix.

let app = @moonapi.App::new()
register(app)
let account = @moonapi.Router::new()
register_router(account)
app.include_router(account, prefix="/account")
let hit = (verb : String, path : String) => {
  let req : @moonasgi.Request = {
    http_method: verb,
    path,
    query_string: b"",
    headers: [],
    body: b"",
View source on GitHub →

Streaming & realtime

Hold a connection open and push data as it arrives.

17-sse

Server-sent events

Stream events to the client and honour the Last-Event-ID header for a clean resume.

let app = @moonapi.App::new()
app.stream("/events", ctx => {
  let resumed = ctx.last_event_id() is Some(_)
  @moonapi.sse(e => {
    if resumed {
      e.send(@sse.Event::of("resumed"))
    }
    for ev in events {
      e.send(ev)
    }
  })
})
View source on GitHub →
18-websocket

WebSockets

Hold a live WebSocket connection in a room route and a streaming sum route.

let app = @moonapi.App::new()
app.websocket("/ws/{room}", room)
app.websocket("/sum", sum)
let client = @testing.Client::new(app.to_asgi())
println("room /ws/233:")
client.websocket(path="/ws/233", subprotocols=["chat", "superchat"], s => {
  println("  subprotocol: \{s.subprotocol.unwrap_or("-")}")
  println("  " + s.receive_text())
  s.send_text("hi")
  println("  " + s.receive_text())
  s.send_bytes(b"\x02\x33\x33")
  println("  bytes back: \{s.receive_bytes().length()}")
View source on GitHub →

Templates

Render HTML from templates and build links by route name.

28-templates

Render HTML templates

Render pages with the template engine, extend a base layout, and build URLs by name.

let app = @moonapi.App::new()
let pages = @templates.Templates::new(
  @moonpug.Env::new(load=name => {
    match name {
      "base.html" =>
        Some(
          "<link rel=\"stylesheet\" href=\"/static/site.css\"><h1>{% block title %}{% endblock %}</h1>{% block body %}{% endblock %}",
        )
      "user.html" =>
        Some(
          "{% extends 'base.html' %}{% block title %}{{ name }}{% endblock %}{% block body %}<a href=\"{{ url_for('user', id=id|int + 1) }}\">next</a> from {{ request.path }}{% endblock %}",
        )
View source on GitHub →

OpenAPI & docs UI

Generate the contract and serve it where tools can read it.

03-openapi-versions

Three spec versions from one app

The same routes serialise as Swagger 2.0, OpenAPI 3.0 and OpenAPI 3.1 — pick the dialect a tool expects.

let app = widget_app()
app.get(
  "/legacy",
  _ctx => @moonapi.text(200, "old"),
  summary="the legacy list",
  tags=["misc"],
  deprecated=true,
)
println("--- Swagger 2.0 (definitions) ---")
println(app.openapi_json(version=Swagger20))
println("--- OpenAPI 3.0.3 (components/schemas) ---")
println(app.openapi_json(version=OpenApi30))
View source on GitHub →
22-docs-ui

Serve the docs UI

Enable the bundled UI and hit /openapi.json, /docs and /redoc for the live specification.

let app = @moonapi.App::new()
app.get(
  "/greet",
  _ctx => @moonapi.text(200, "hello, heke1228"),
  summary="greet a user",
)
app.describe(
  title="greet api",
  description="One route, documented three ways.",
  api_version="1.0.0",
)
app.enable_docs()
View source on GitHub →

Full system

Everything above, composed into one running service.

25-full-example

One service, every feature

An integrated application: routers, auth, typed bodies, layers, DI, files, tasks, SSE, WebSockets and OpenAPI.

  let app = @application.build()
  println("moonapi serving on http://127.0.0.1:12000")
  @mooncat.serve(app.to_asgi()) catch {
    err => println("mooncat stopped: \{err}")
  }
}
View source on GitHub →