crescent

    Crescent: A web framework for MoonBit.

    http
    server
    web
    framework
    async
    ai-friendly
    Download zip
    Author
    Version
    0.11.1
    License
    Apache-2.0
    Last updated
    10 days ago
    Downloads
    500

    Dependencies

    #Crescent

    A web framework for MoonBit. Type-safe and AI-agent friendly.

    Hard fork of oboard/mocket by oboard. Credits at the bottom.

    Targets: native and wasm1 (--target wasm). The HTTP server, fetch, WebSocket, and static-file packages use moonbitlang/async on both targets. JS and wasm-gc are not supported. Native remains the preferred target in moon.mod.

    #Install

    moon add bobzhang/crescent

    #Hello World

    ///|
    async fn main {
    let app = @crescent.App()
    app.get("/", _ => "Hello, Crescent!")
    app.serve(port=4000)
    }

    moon run . --target native # Visit http://localhost:4000


    #Building a Todo API

    This walkthrough builds a complete REST API step by step.

    #Define your types

    Types with derive(ToJson, FromJson) are your API contract. The same types compile to native (backend) and JS (frontend) — no code generation needed.

    ///|
    struct Todo {
    id : Int
    title : String
    done : Bool
    } derive(ToJson, FromJson)

    ///|
    struct CreateTodo {
    title : String
    } derive(FromJson)

    ///|
    #warnings("-unnecessary_annotation")
    struct JsonDemoUser {
    name : String
    age : Int
    } derive(ToJson, FromJson, Eq)

    ///|
    test "json_value sets content-type and serializes body" {
    let user = JsonDemoUser::{ name: "Alice", age: 30, }
    let res = HttpResponse::ok().json_value(user)
    debug_inspect(
    res,
    content=(
    #|{
    #| status_code: OK,
    #| headers: { "Content-Type": "application/json; charset=utf-8" },
    #| cookies: {},
    #| raw_body: ...,
    #|}
    ),
    )

    debug_inspect(
    @utf8.decode(res.raw_body),
    content=(
    #|"{\"name\":\"Alice\",\"age\":30}"
    ),
    )
    }

    #Wire up the routes

    All handlers automatically map errors to JSON — bad JSON returns 400, raise HttpError(...) returns a structured error, anything else returns 500. You never write error-handling boilerplate.

    ///|
    #warnings("-unused_value")
    fn build_app() -> @crescent.App {
    let app = @crescent.App()
    let todos : Array[Todo] = [{ id: 1, title: "Learn MoonBit", done: false, }]
    let next_id = Ref::Ref(2)

    // List all
    app.get("/api/todos", _ => HttpResponse::ok().json_value(todos))

    // Get by ID — require_param_int auto-returns 400 for "abc"
    app.get("/api/todos/:id", event => {
    let id = event.require_param_int("id")
    for todo in todos {
    if todo.id == id {
    return HttpResponse::ok().json_value(todo)
    }
    }
    raise HttpError::HttpError(NotFound, "todo \{id} not found")
    })

    // Create — event.json() auto-returns 400 for invalid JSON
    app.post("/api/todos", event => {
    let input : CreateTodo = event.json()
    let todo = Todo::{ id: next_id.val, title: input.title, done: false, }
    next_id.val 1
    todos.push(todo)
    HttpResponse::created().json_value(todo)
    })

    // Health check — get_raw for handlers that never raise
    app.get_raw("/health", _ => "ok")

    app
    }

    #Add middleware

    Register middleware before routes. They execute in onion order (first registered = outermost layer).

    ///|
    fn _build_app() -> @crescent.App {
    let app = @crescent.App()

    // Security headers on every response
    app.use_middleware(@middleware.security_headers())

    // Unique request ID for distributed tracing
    app.use_middleware(@middleware.request_id())

    // Request logging
    app.use_middleware((event, next) => {
    let start = @async.now()
    let res = next()
    let ms = @async.now() - start
    println("\{event.req.http_method} \{event.req.url} \{ms}ms")
    res
    })

    // Auth only on /api routes
    app.use_middleware(
    (event, next) => {
    match event.req.get_header("Authorization") {
    Some(_) => next()
    None => HttpResponse::unauthorized()
    }
    },
    base_path="/api",
    )

    // ... register routes ...
    app
    }

    ///|
    async test "security headers middleware" {
    let app = @crescent.App()
    app.use_middleware(@middleware.security_headers())
    app.get_raw("/test", fn(_) noraise { "ok" })
    let client = @test_client.TestClient(app)
    let res = client.get("/test")
    guard res.headers
    is { "X-Content-Type-Options": "nosniff", "X-Frame-Options": "DENY", .. } else {
    fail("missing expected security headers")
    }
    }

    ///|
    async test "request ID middleware" {
    let app = @crescent.App()
    app.use_middleware(@middleware.request_id())
    app.get_raw("/test", fn(_) noraise { "ok" })
    let client = @test_client.TestClient(app)
    let res = client.get("/test")
    assert_true(res.headers.get("X-Request-Id") is Some(_))
    }

    #Test without a network

    TestClient dispatches requests in-process. No ports, no sockets, fast CI.

    ///|
    async test "create a todo" {
    let app = build_app()
    let client = @test_client.TestClient(app)

    let res = client.post("/api/todos", body=b"{\"title\":\"Write tests\"}")
    assert_eq(res.status, Created)

    let todo : Todo = res.body_json()
    assert_eq(todo.title, "Write tests")
    assert_eq(todo.done, false)
    }

    ///|
    async test "invalid ID returns 400" {
    let client = @test_client.TestClient(build_app())
    let res = client.get("/api/todos/abc")
    assert_eq(res.status, BadRequest)
    }

    ///|
    async test "missing todo returns 404" {
    let client = @test_client.TestClient(build_app())
    let res = client.get("/api/todos/999")
    assert_eq(res.status, NotFound)
    }

    ///|
    async test "bad JSON returns 400" {
    let client = @test_client.TestClient(build_app())
    let res = client.post("/api/todos", body=b"not json")
    assert_eq(res.status, BadRequest)
    }

    ///|
    async test "security headers are present" {
    let client = @test_client.TestClient(build_app())
    let res = client.get("/health")
    assert_eq(res.headers.get("X-Content-Type-Options"), Some("nosniff"))
    }

    ///|
    #warnings("-unnecessary_annotation")
    struct TodoInput {
    title : String
    } derive(FromJson, ToJson)

    ///|
    async test "typed handler auto-maps errors" {
    let app = @crescent.App()
    app.post("/todos", event => {
    let input : TodoInput = event.json()
    HttpResponse::created().json_value(input)
    })
    let client = @test_client.TestClient(app)

    // Valid request
    let res = client.post("/todos", body=b"{\"title\":\"Learn MoonBit\"}")
    assert_eq(res.status, Created)

    // Invalid JSON -> automatic 400
    let res2 = client.post("/todos", body=b"not json")
    assert_eq(res2.status, BadRequest)
    }

    #Start the server

    ///|
    async fn main {
    let app = build_app()
    println("Listening on http://localhost:4000")
    app.serve(port=4000)
    }

    Try it:

    curl localhost:4000/api/todos curl -X POST localhost:4000/api/todos -d '{"title":"Write docs"}' curl localhost:4000/api/todos/1 curl localhost:4000/api/todos/abc # 400: must be a valid integer curl -X POST localhost:4000/api/todos -d 'not json' # 400: parse error curl localhost:4000/health # "ok"


    #CRUD with resource()

    When your API follows REST conventions, register all 5 routes in one call:

    app.resource("/api/todos", ResourceConfig(
    list = fn(_) { HttpResponse::ok().json_value(all_todos()) },
    get = fn(e) {
    let id = e.require_param_int("id")
    HttpResponse::ok().json_value(find_todo(id))
    },
    create = fn(e) {
    let input : CreateTodo = e.json()
    HttpResponse::created().json_value(insert_todo(input))
    },
    update = fn(e) {
    let id = e.require_param_int("id")
    let input : CreateTodo = e.json()
    HttpResponse::ok().json_value(update_todo(id, input))
    },
    delete = fn(e) {
    let id = e.require_param_int("id")
    delete_todo(id)
    HttpResponse::no_content()
    },
    ))

    This registers GET /api/todos, GET /api/todos/:id, POST /api/todos, PUT /api/todos/:id, DELETE /api/todos/:id. All handlers omitted from ResourceConfig(...) are simply not registered.

    ///|
    #warnings("-unnecessary_annotation")
    struct ResItem {
    id : Int
    name : String
    } derive(ToJson, FromJson, Eq, Debug)

    ///|
    #warnings("-unnecessary_annotation")
    struct CreateResItem {
    name : String
    } derive(FromJson, ToJson)

    ///|
    async test "resource CRUD" {
    let items : Array[ResItem] = [{ id: 1, name: "Alpha", }]
    let app = @crescent.App()
    app.resource(
    "/items",
    ResourceConfig(
    list=_ => HttpResponse::ok().json_value(items),
    get=event => {
    let id = event.require_param_int("id")
    for item in items {
    if item.id == id {
    return HttpResponse::ok().json_value(item)
    }
    }
    raise HttpError::HttpError(NotFound, "not found")
    },
    create=event => {
    let input : CreateResItem = event.json()
    let item = ResItem::{ id: 2, name: input.name, }
    items.push(item)
    HttpResponse::created().json_value(item)
    },
    ),
    )
    let client = @test_client.TestClient(app)

    // List
    let res = client.get("/items")
    assert_eq(res.status, OK)
    let list : Array[ResItem] = res.body_json()
    assert_eq(list.length(), 1)

    // Create
    let res2 = client.post("/items", body=b"{\"name\":\"Beta\"}")
    assert_eq(res2.status, Created)
    let created : ResItem = res2.body_json()
    assert_eq(created.name, "Beta")

    // Get by ID
    let res3 = client.get("/items/1")
    assert_eq(res3.status, OK)
    let item : ResItem = res3.body_json()
    // TODO(upstream): change the bound of `assert_eq` from `Show` to `Debug`
    debug_inspect(
    item,
    content=(
    #|{ id: 1, name: "Alpha" }
    ),
    )

    // Not found
    let res4 = client.get("/items/999")
    assert_eq(res4.status, NotFound)
    }


    #Route Groups

    Share a path prefix and middleware across related routes:

    app.group("/api/v1", fn(api) {
    api.use_middleware(require_auth())

    api.get("/users", _ => HttpResponse::ok().json_value(users))
    api.get("/users/:id", fn(event) {
    let id = event.require_param_int("id")
    HttpResponse::ok().json_value(find_user(id))
    })
    api.post("/users", fn(event) {
    let user : CreateUser = event.json()
    HttpResponse::created().json_value(save_user(user))
    })
    })
    // Routes: GET /api/v1/users, GET /api/v1/users/:id, POST /api/v1/users
    // require_auth() only runs for /api/v1/* requests

    #Route Patterns

    PatternExample URLCaptures
    /users/users(exact match)
    /users/:id/users/42event.param("id") = "42"
    /users/:id/posts/:pid/users/1/posts/99two params
    /files/*/files/readme.txtone segment in _
    /static/**/static/css/main.cssany depth in _

    Matching uses a radix tree — O(path length), not O(number of routes).

    ///|
    test "compiled route matching" {
    let route = @router.CompiledRoute("/users/:id/posts/:postId")
    assert_eq(
    route.match_path("/users/42/posts/99"),
    Some({ "id": "42", "postId": "99" }),
    )
    assert_eq(route.match_path("/users/42"), None)
    }

    ///|
    test "static routes are flagged" {
    let static_route = @router.CompiledRoute("/api/health")
    assert_true(static_route.is_static)
    let dynamic_route = @router.CompiledRoute("/api/:version")
    assert_true(dynamic_route.is_static == false)
    }


    #WebSocket

    app.ws("/chat", fn(event) {
    match event {
    Open(peer) => {
    println("Connected: \{peer.to_string()}")
    peer.subscribe("chat-room")
    }
    Message(peer, Text(msg)) =>
    peer.publish("chat-room", msg) // broadcast to all subscribers
    Message(peer, Binary(data)) =>
    peer.binary(data) // echo binary back
    Close(peer) =>
    println("Disconnected: \{peer.to_string()}")
    }
    })

    WebSocketPeer methods: text(msg), binary(data), subscribe(channel), unsubscribe(channel), publish(channel, msg).

    #Static Files

    app.static_assets("/assets", @static_file.StaticFileProvider(path="./public"))

    Features: ETag caching, If-Modified-Since / If-None-Match support, Accept-Encoding content negotiation, path traversal protection, directory index fallback (index.html, index.htm, ...).

    #trait ServeStaticProvider

    ///|
    pub(open) trait ServeStaticProvider {
    async fn get_meta(Self, String) -> StaticAssetMeta?
    async fn get_contents(Self, String) -> &Responder
    fn get_type(Self, String) -> String?
    fn get_encodings(Self) -> Map[String, String]
    fn get_index_names(Self) -> Array[String]
    fn get_fallthrough(Self) -> Bool
    }

    static_assets accepts any type that implements ServeStaticProvider. The bundled @static_file.StaticFileProvider serves from the filesystem, but custom providers can pull from S3, an embedded asset bundle, a zip file, a CDN cache, etc. The path argument to each method is the asset's URL path (already stripped of the mount prefix and resolved against any index filenames).

    MethodPurpose
    get_metaResolve the path to asset metadata (size, mtime, ETag); None means "not found"
    get_contentsProduce the response body for the resolved asset
    get_typeReturn the Content-Type for the path (None skips the header)
    get_encodingsProvider-wide Content-Encoding → variant suffix map (e.g. gzip.gz)
    get_index_namesFilenames to try when the request points at a directory
    get_fallthroughIf true, a miss falls through to the next route instead of 404

    #Cookies

    // Set with attributes
    event.res.set_cookie("session", "abc123",
    max_age=3600, http_only=true, same_site=Lax)

    // Read
    let user = match event.req.get_cookie("session") {
    Some(cookie) => find_user_by_session(cookie.value)
    None => anonymous_user()
    }

    // Delete (sets Max-Age=0)
    event.res.delete_cookie("session")

    ///|
    test "set and format cookie" {
    let res = @crescent.HttpResponse(status_code=OK)
    res.set_cookie(
    "session",
    "abc123",
    max_age=3600,
    http_only=true,
    same_site=Lax,
    )
    assert_true(res.cookies.get("session") is Some(_))
    }

    #HTTP Client (Fetch)

    Make outbound HTTP requests from your handlers:

    app.get("/api/weather/:city", fn(event) {
    let city = event.require_param("city")
    let res = @fetch.get("https://api.weather.example/v1/\{city}")
    let weather : WeatherData = res.read_body()
    HttpResponse::ok().json_value(weather)
    })

    Methods: @fetch.get, @fetch.post, @fetch.put, @fetch.patch, @fetch.delete, @fetch.head. All accept optional data, headers, credentials, and mode parameters.

    #CORS

    // Allow all origins (default)
    app.use_middleware(@cors.handle_cors())

    // Restrict to specific origin
    app.use_middleware(@cors.handle_cors(
    origin="https://myapp.com",
    methods="GET,POST",
    credentials=true,
    max_age=3600,
    ))

    #Server Configuration

    #Request limits

    app.serve(port=4000, options=NativeServeOptions(
    max_connections=1000, // concurrent connection limit
    max_request_body_bytes=1_048_576, // 1MB body size limit (413 if exceeded)
    request_body_read_timeout_ms=5000, // 5s read timeout (408 if exceeded)
    ))

    #WebSocket options

    app.serve(port=4000, options=NativeServeOptions(
    websocket_max_message_bytes=65536, // max inbound message size
    websocket_outgoing_queue_capacity=100, // outbound buffer per connection
    websocket_overflow_policy=DropOldest, // or DropLatest
    websocket_read_timeout_ms=30000, // close idle connections after 30s
    ))

    #Graceful shutdown

    let shutdown = @async.Queue()

    // In another task: shutdown.put(()) to stop the server
    app.serve(port=4000, shutdown~)

    #Serve on an existing server

    let addr = @socket.Addr::parse("0.0.0.0:4000")
    let server = @http.Server(addr, reuse_addr=true)
    app.serve_on(server)


    #Response Helpers

    HttpResponse::ok() // 200
    HttpResponse::created() // 201
    HttpResponse::no_content() // 204
    HttpResponse::bad_request() // 400
    HttpResponse::unauthorized() // 401
    HttpResponse::forbidden() // 403
    HttpResponse::not_found() // 404
    HttpResponse::error(BadRequest, "message") // JSON error body
    HttpResponse::redirect("/new-path") // 301
    HttpResponse::redirect_temporary("/temp") // 302
    HttpResponse::redirect_307("/preserve") // 307 (preserves method)
    HttpResponse::redirect_308("/permanent") // 308 (permanent, preserves method)

    // Fluent chaining
    HttpResponse::ok()
    .header("Cache-Control", "max-age=3600")
    .json_value(data)

    ///|
    test "response helpers" {
    let ok = HttpResponse::ok()
    assert_eq(ok.status_code, OK)

    let created = HttpResponse::created()
    assert_eq(created.status_code, Created)

    let not_found = HttpResponse::not_found()
    assert_eq(not_found.status_code, NotFound)

    let no_content = HttpResponse::no_content()
    assert_eq(no_content.status_code, NoContent)

    let bad_request = HttpResponse::bad_request()
    assert_eq(bad_request.status_code, BadRequest)
    }

    ///|
    test "fluent response building" {
    let res = HttpResponse::ok()
    .header("X-Custom", "value")
    .header("Cache-Control", "max-age=3600")
    guard res.headers
    is { "X-Custom": "value", "Cache-Control": "max-age=3600", .. } else {
    fail("expected fluent headers to be set")
    }
    }

    ///|
    test "redirect helpers" {
    let r301 = HttpResponse::redirect("/new")
    assert_eq(r301.status_code, MovedPermanently)
    assert_eq(r301.headers.get("Location"), Some("/new"))

    let r302 = HttpResponse::redirect_temporary("/temp")
    assert_eq(r302.status_code, Found)
    }

    #Error Handling

    All get/post/put/patch/delete handlers catch errors automatically:

    What you writeWhat the client gets
    raise HttpError(BadRequest, "invalid email")400 {"error":{"status":400,"message":"invalid email"}}
    raise HttpError(NotFound, "not found")404 with JSON body
    event.json() on bad input400 with parse error message
    event.require_param_int("id") on "abc"400 "must be a valid integer"
    Any unhandled error500 Internal Server Error

    For handlers that should never raise (health checks, plain text), use get_raw:

    app.get_raw("/health", fn(_) noraise { "ok" })

    For custom error responses, use try_json:

    app.post("/users", fn(event) {
    match event.try_json() {
    Ok(user) => HttpResponse::ok().json_value(user)
    Err(msg) => HttpResponse::error(BadRequest, "Invalid user: \{msg}")
    }
    })

    #Built-in Middleware

    Middleware implementations live in the bobzhang/crescent/middleware sub-package.

    MiddlewareWhat it does
    @middleware.security_headers()X-Content-Type-Options: nosniff, X-Frame-Options: DENY, X-XSS-Protection: 0, Referrer-Policy: strict-origin-when-cross-origin
    @middleware.request_id()Adds X-Request-Id header; preserves incoming IDs for distributed tracing. Access via event.request_id()
    @middleware.rate_limit(requests_per_window~, window_ms~)Fixed-window rate limiter. Returns 429 Too Many Requests with a Retry-After header when the limit is exceeded.
    @cors.handle_cors()Full CORS support: preflight OPTIONS handling, configurable origins/methods/credentials

    Writing custom middleware:

    ///|
    fn rate_limiter() -> Middleware {
    let count = Ref(0)
    fn(event, next) {
    count.val 1
    if count.val > 100 {
    return HttpResponse::error(TooManyRequests, "slow down")
    }
    next()
    }
    }


    #API Quick Reference

    #Parameters

    MethodReturnsOn missing/invalid
    event.param("name")String?None
    event.param_int("id")Int?None
    event.param_int64("id")Int64?None
    event.require_param("name")Stringraises 400
    event.require_param_int("id")Intraises 400
    event.require_param_int64("id")Int64raises 400

    ///|
    test "param and param_int" {
    let event = Event::{
    req: HttpRequest(Get, "/", {}, raw_body=b""),
    res: HttpResponse(status_code=OK),
    params: { "id": "42", "name": "alice" },
    }
    assert_eq(event.param("name"), Some("alice"))
    assert_eq(event.param_int("id"), Some(42))
    assert_eq(event.param("missing"), None)
    }

    ///|
    test "require_param raises on missing" {
    let event = Event::{
    req: HttpRequest(Get, "/", {}, raw_body=b""),
    res: HttpResponse(status_code=OK),
    params: {},
    }
    @test.assert_raise(() => event.require_param_int("id"))
    }

    #Body & Query

    MethodReturnsNotes
    event.json[T]()TRaises on invalid JSON
    event.try_json[T]()Result[T, String]For custom error handling
    event.req.body[T]()TVia BodyReader trait (String, Bytes, Json)
    event.req.get_query("key")String?URL-decoded, cached
    event.req.query_params()Map[String, String]All query params, cached
    event.req.path()StringPath without query string, cached
    event.req.content_type()String?Content-Type header value
    event.request_id()String?Requires @middleware.request_id() middleware

    ///|
    #warnings("-unnecessary_annotation")
    struct ReadmeCreateUser {
    name : String
    age : Int
    } derive(FromJson)

    ///|
    test "json parsing from request body" {
    let req = @crescent.HttpRequest(
    Post,
    "/users",
    {},
    raw_body=b"{\"name\":\"Bob\",\"age\":25}",
    )
    let user : ReadmeCreateUser = req.json()
    assert_eq(user.name, "Bob")
    assert_eq(user.age, 25)
    }

    ///|
    test "try_json returns Result" {
    let req = @crescent.HttpRequest(Post, "/", {}, raw_body=b"not json")
    let result : Result[ReadmeCreateUser, String] = req.try_json()
    assert_true(result is Err(_))
    }

    ///|
    test "path extracts from request target" {
    let req = @crescent.HttpRequest(Get, "/api/users?q=test", {}, raw_body=b"")
    let path = req.path()
    assert_eq(path, "/api/users")
    }

    ///|
    test "query_params cached and decoded" {
    let req = @crescent.HttpRequest(
    Get,
    "/search?q=hello%20world&lang=en",
    {},
    raw_body=b"",
    )
    assert_eq(req.get_query("q"), Some("hello world"))
    assert_eq(req.get_query("lang"), Some("en"))
    assert_eq(req.get_query("missing"), None)
    }

    #HttpMethod Enum

    Pattern match on the request method — no string comparisons:

    match event.req.http_method {
    Get => "read"
    Post | Put | Patch => "write"
    Delete => "delete"
    _ => "other"
    }

    ///|
    test "HttpMethod round-trip" {
    let meth : HttpMethod = Post
    assert_eq(meth.to_method_string(), "POST")
    assert_eq(HttpMethod::from_string("POST"), Post)
    }

    ///|
    test "HttpMethod pattern matching" {
    let req = @crescent.HttpRequest(Get, "/", {}, raw_body=b"")
    let label = match req.http_method {
    Get => "read"
    Post => "write"
    _ => "other"
    }
    assert_eq(label, "read")
    }

    #Performance

    • Radix tree routing — O(path length) dynamic route lookup
    • Pre-compiled patterns — route templates parsed at registration, not per request
    • Cached parsing — path and query string parsed once per request
    • Zero-alloc headers — case-insensitive ASCII comparison without string allocation
    • Direct byte outputBytes and HttpResponse skip the intermediate buffer

    #Packages

    bobzhang/crescent — Core: routing, middleware, serving, WebSocket bobzhang/crescent/httputil — HTTP protocol: headers, dates, URL encoding bobzhang/crescent/cors — CORS middleware bobzhang/crescent/fetch — HTTP client bobzhang/crescent/static_file — Static file provider (filesystem) bobzhang/crescent/test_client — In-process test client (no network I/O) bobzhang/crescent/uri — RFC 3986 URI parser

    #Credits

    Crescent is a hard fork of oboard/mocket by oboard. The original framework established the core architecture: Express-style routing, onion middleware, WebSocket support, static file serving, and the async-native design built on moonbitlang/async.

    #License

    Apache-2.0

    BodyReader

    Trait for types that can be deserialized from an HTTP request body.

    HttpMethod

    HTTP request methods as a type-safe enum.

    Using this enum instead of raw strings prevents typos like "DLETE" or "OPTONS" and enables pattern matching in handlers.

    HttpRequest

    An incoming HTTP request containing the method, URL, headers, and body.

    HttpResponse

    An outgoing HTTP response containing the status code, headers, cookies, and body.

    NativeWebSocketOverflowPolicy

    Policy for handling a full outbound WebSocket message queue.

    Responder

    Trait for types that can be sent as an HTTP response body.

    StatusCode

    HTTP status codes as registered with IANA. See: https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml

    WebSocketAggregatedMessage

    A fully assembled WebSocket message, either text or binary.

    WebSocketEvent

    Events delivered to a WebSocket handler: open, message, or close.

    WebSocketHandler

    Handler function type for WebSocket route events.

    WebSocketPeer

    Represents a connected WebSocket client with its connection ID, subscribed channels, and route parameters captured from the upgrade URL.

    ServeStaticProvider

    pub(open) trait ServeStaticProvider {
    async fn get_meta(Self, id : String) -> StaticAssetMeta? noraise
    async fn get_contents(Self, id : String) -> &
    Responder
    noraise
    fn get_type(Self, ext : String) -> String?
    fn get_encodings(Self) -> Map[String, String]
    fn get_index_names(Self) -> Array[String]
    fn get_fallthrough(Self) -> Bool
    }

    Trait for providing static file serving capabilities, including asset lookup, content retrieval, and MIME type resolution.

    ExecError

    pub suberror ExecError derive(
    Debug
    )

    Error type for execution failures such as subprocess errors.

    HttpError

    pub(all) suberror HttpError {
    HttpError(
    StatusCode
    , String)
    } derive(
    Debug
    )

    Error type for handlers that want automatic HTTP error responses.

    IOError

    pub suberror IOError derive(
    Debug
    )

    Error type for I/O operations such as file reads or writes.

    IOError::to_repr

    NativeServeError

    pub suberror NativeServeError {
    InvalidMaxConnections(Int)
    InvalidMaxRequestBodyBytes(Int)
    InvalidRequestBodyReadTimeoutMs(Int)
    InvalidHandlerTimeoutMs(Int)
    InvalidShutdownTimeoutMs(Int)
    InvalidWebSocketMaxMessageBytes(Int)
    InvalidWebSocketOutgoingQueueCapacity(Int)
    InvalidWebSocketReadTimeoutMs(Int)
    } derive(ToJson,
    Debug
    )

    Error type for invalid native server configuration values.

    NativeServeError::to_json

    NetworkError

    pub suberror NetworkError derive(
    Debug
    )

    Error type for network-related failures such as connection errors.

    App

    pub struct App {
    // private fields
    }

    The main application type that holds route mappings, middleware, and WebSocket handlers.

    Routing state is split by access pattern:
    • static_routesMap[method, Map[path, handler]] for O(1) static lookup
    • dynamic_routes — radix tree for O(path_length) dynamic lookup
    • route_keys — flat list of (method, path) for introspection via routes()

    App::App

    fn App::App(base_path? : String) -> App

    Creates a new App application instance with an optional base path prefix.

    App::all

    fn App::all(self : App, path : String, handler : async (Event) -> &
    Responder
    ) -> Unit

    Registers a handler matching all HTTP methods with automatic error-to-JSON mapping.

    App::all_raw

    fn App::all_raw(self : App, path : String, handler : HttpHandler) -> Unit

    Registers a noraise handler that matches all HTTP methods.

    App::connect

    fn App::connect(self : App, path : String, handler : async (Event) -> &
    Responder
    ) -> Unit

    Registers a CONNECT handler with automatic error-to-JSON mapping.

    App::connect_raw

    fn App::connect_raw(self : App, path : String, handler : HttpHandler) -> Unit

    Registers a noraise CONNECT handler.

    App::delete

    fn App::delete(self : App, path : String, handler : async (Event) -> &
    Responder
    ) -> Unit

    Registers a DELETE handler with automatic error-to-JSON mapping.

    App::delete_raw

    fn App::delete_raw(self : App, path : String, handler : HttpHandler) -> Unit

    Registers a noraise DELETE handler.

    App::dispatch

    async fn App::dispatch(self : App, http_method : String, url : String, headers : Map[String, String], body : Bytes) -> (
    StatusCode
    , Map[String, String], Bytes)

    Dispatches a synthetic HTTP request through the full routing + middleware pipeline and returns (status_code, headers, body_bytes).

    App::get

    fn App::get(self : App, path : String, handler : async (Event) -> &
    Responder
    ) -> Unit

    Registers a GET handler with automatic error-to-JSON mapping.

    App::get_raw

    fn App::get_raw(self : App, path : String, handler : HttpHandler) -> Unit

    Registers a noraise GET handler. For most use cases, prefer get().

    App::group

    fn App::group(self : App, base_path : String, configure : (App) -> Unit) -> Unit

    Creates a route group with a shared base path prefix, merging its routes and middleware into the app.

    App::has_not_found_handler

    fn App::has_not_found_handler(self : App) -> Bool

    Returns true if any custom not-found handler has been registered, either as the catch-all via set_not_found_handler or via a group.

    App::head

    fn App::head(self : App, path : String, handler : async (Event) -> &
    Responder
    ) -> Unit

    Registers a HEAD handler with automatic error-to-JSON mapping.

    App::head_raw

    fn App::head_raw(self : App, path : String, handler : HttpHandler) -> Unit

    Registers a noraise HEAD handler.

    App::on

    fn App::on(self : App, http_method : String, path : String, handler : HttpHandler) -> Unit

    Registers a route handler for the given HTTP method and path.

    App::options

    fn App::options(self : App, path : String, handler : async (Event) -> &
    Responder
    ) -> Unit

    Registers an OPTIONS handler with automatic error-to-JSON mapping.

    App::options_raw

    fn App::options_raw(self : App, path : String, handler : HttpHandler) -> Unit

    Registers a noraise OPTIONS handler.

    App::patch

    fn App::patch(self : App, path : String, handler : async (Event) -> &
    Responder
    ) -> Unit

    Registers a PATCH handler with automatic error-to-JSON mapping.

    App::patch_raw

    fn App::patch_raw(self : App, path : String, handler : HttpHandler) -> Unit

    Registers a noraise PATCH handler.

    App::post

    fn App::post(self : App, path : String, handler : async (Event) -> &
    Responder
    ) -> Unit

    Registers a POST handler with automatic error-to-JSON mapping.

    App::post_raw

    fn App::post_raw(self : App, path : String, handler : HttpHandler) -> Unit

    Registers a noraise POST handler.

    App::put

    fn App::put(self : App, path : String, handler : async (Event) -> &
    Responder
    ) -> Unit

    Registers a PUT handler with automatic error-to-JSON mapping.

    App::put_raw

    fn App::put_raw(self : App, path : String, handler : HttpHandler) -> Unit

    Registers a noraise PUT handler.

    App::resource

    fn App::resource(self : App, path : String, config : ResourceConfig) -> Unit

    Registers standard REST routes (list, get, create, update, delete) for a resource path.

    App::routes

    fn App::routes(self : App) -> Iter[(String, String)]

    Returns an iterator over the registered route keys as (method, path) pairs.

    App::serve

    async fn App::serve(self : App, port~ : Int, shutdown? :
    Queue
    [Unit], options? : NativeServeOptions) -> Unit

    Starts serving HTTP requests on the given port. max_connections, if present, limits the number of client connections handled in parallel by the underlying @http.Server. max_request_body_bytes, if present, rejects oversized request bodies with 413 Request Entity Too Large. request_body_read_timeout_ms, if present, rejects slow request bodies with 408 Request Timeout. websocket_max_message_bytes, if present, closes websocket connections with 1009 MessageTooBig when an inbound message exceeds that many bytes. websocket_outgoing_queue_capacity, if present, bounds buffered outbound websocket messages per connection. websocket_overflow_policy, if present, chooses whether a full outbound websocket queue drops the oldest or latest message. websocket_read_timeout_ms, if present, closes websocket connections after that many milliseconds waiting for the next inbound message. When shutdown is supplied, serving stops once the queue receives a unit or is closed. See serve_on for shutdown semantics.

    App::serve_on

    async fn App::serve_on(self : App, server :
    Server
    , shutdown? :
    Queue
    [Unit], options? : NativeServeOptions) -> Unit

    Starts serving HTTP requests on the given server. max_connections, if present, limits the number of client connections handled in parallel by the underlying @http.Server. max_request_body_bytes, if present, rejects oversized request bodies with 413 Request Entity Too Large. request_body_read_timeout_ms, if present, rejects slow request bodies with 408 Request Timeout. websocket_max_message_bytes, if present, closes websocket connections with 1009 MessageTooBig when an inbound message exceeds that many bytes. websocket_outgoing_queue_capacity, if present, bounds buffered outbound websocket messages per connection. websocket_overflow_policy, if present, chooses whether a full outbound websocket queue drops the oldest or latest message. websocket_read_timeout_ms, if present, closes websocket connections after that many milliseconds waiting for the next inbound message. When shutdown is supplied, serving stops once the queue receives a unit or is closed. shutdown.put(()) wakes one waiter; shutdown.close() broadcasts to all waiters. When shutdown_timeout_ms is set, shutdown is graceful: the server stops accepting new connections and waits up to that many milliseconds for in-flight requests to complete before force-cancelling. Otherwise shutdown is cancellation-based (active requests are aborted immediately).

    App::set_not_found_handler

    fn App::set_not_found_handler(self : App, handler : HttpHandler) -> Unit

    Sets a custom handler for 404 Not Found responses.

    App::static_assets

    fn App::static_assets(self : App, path : String, provider : &ServeStaticProvider) -> Unit

    Mounts a static asset provider at the given path, serving files with ETag/Last-Modified caching and content negotiation.

    App::trace

    fn App::trace(self : App, path : String, handler : async (Event) -> &
    Responder
    ) -> Unit

    Registers a TRACE handler with automatic error-to-JSON mapping.

    App::trace_raw

    fn App::trace_raw(self : App, path : String, handler : HttpHandler) -> Unit

    Registers a noraise TRACE handler.

    App::use_middleware

    fn App::use_middleware(self : App, middleware : Middleware, base_path? : String) -> Unit

    Adds a middleware to the app, optionally scoped to a base path prefix.

    App::ws

    fn App::ws(self : App, path : String, handler :
    WebSocketHandler
    ) -> Unit

    Registers a WebSocket handler for the given path, matched regardless of HTTP method.

    Dynamic patterns (:param, *, **) are inserted into the same radix tree implementation used for HTTP routes, giving them consistent precedence: static > param > wildcard > globstar. Re-registering the same path overrides the previous handler.

    App::ws_runtime_ids

    fn App::ws_runtime_ids(self : App) -> Array[String]

    Returns all WebSocket runtime IDs scoped to this App instance, in the form <app_id>/serve-<n> (one per active serve_on/serve invocation). Used in tests to verify runtime registration and cleanup; production code should not need this.

    Event

    pub(all) struct Event {
    req :
    HttpRequest

    res :
    HttpResponse

    params : Map[String, StringView]
    }

    The event passed to each HTTP handler, bundling the request, response, and route parameters.

    Event::json

    fn[T :
    FromJson
    ] Event::json(self : Event) -> T raise

    Parses the request body as JSON and deserializes into type T. Shorthand for event.req.json().

    Event::param

    fn Event::param(self : Event, name : String) -> StringView?

    Returns a route parameter value as a String, or None if not found.

    The value is NOT URL-decoded — it's returned exactly as it appeared in the URL. For a decoded value (e.g. hello%20worldhello world), use param_decoded().

    Event::param_decoded

    fn Event::param_decoded(self : Event, name : String) -> String?

    Returns a URL-decoded route parameter value, or None if not found.

    For example, if the URL is /search/hello%20world and the route is /search/:query, this returns Some("hello world").

    Event::param_int

    fn Event::param_int(self : Event, name : String) -> Int?

    Returns a route parameter parsed as an Int, or None if not found or not a valid integer.

    Event::param_int64

    fn Event::param_int64(self : Event, name : String) -> Int64?

    Returns a route parameter parsed as an Int64, or None if not found or not valid.

    Event::request_id

    fn Event::request_id(self : Event) -> String?

    Returns the request ID for this event, or None if the request_id middleware from bobzhang/crescent/middleware is not active.

    Event::require_param

    fn Event::require_param(self : Event, name : String) -> String raise

    Returns a route parameter as a String, or raises HttpError(BadRequest) if missing.

    Event::require_param_int

    fn Event::require_param_int(self : Event, name : String) -> Int raise

    Returns a route parameter parsed as an Int, or raises HttpError(BadRequest) if missing or not a valid integer.

    Event::require_param_int64

    fn Event::require_param_int64(self : Event, name : String) -> Int64 raise

    Returns a route parameter parsed as an Int64, or raises HttpError(BadRequest) if missing or not a valid 64-bit integer.

    Event::try_json

    fn[T :
    FromJson
    ] Event::try_json(self : Event) -> Result[T, String]

    Tries to parse the request body as JSON, returning Ok(T) on success or Err(message) on failure. Shorthand for event.req.try_json().

    HttpHandler

    pub(all) struct HttpHandler(async (Event) -> &
    Responder
    noraise)

    The async handler type for HTTP route callbacks.

    Middleware

    pub(all) struct Middleware(async (Event, MiddlewareNext) -> &
    Responder
    noraise)

    A middleware function that receives a request event and a next continuation, and returns a response.

    MiddlewareNext

    pub(all) struct MiddlewareNext(async () -> &
    Responder
    noraise)

    The continuation function passed to middleware, calling the next handler in the chain.

    NativeServeOptions

    pub struct NativeServeOptions {
    max_connections : Int?
    max_request_body_bytes : Int?
    request_body_read_timeout_ms : Int?
    handler_timeout_ms : Int?
    shutdown_timeout_ms : Int?
    websocket_outgoing_queue_capacity : Int?
    websocket_overflow_policy :
    NativeWebSocketOverflowPolicy
    ?
    websocket_read_timeout_ms : Int?
    websocket_max_message_bytes : Int?
    }

    Configuration options for the native async HTTP server runtime. The constructor validates all values eagerly and raises on invalid input.

    NativeServeOptions::NativeServeOptions

    fn NativeServeOptions::NativeServeOptions(max_connections? : Int, max_request_body_bytes? : Int, request_body_read_timeout_ms? : Int, handler_timeout_ms? : Int, shutdown_timeout_ms? : Int, websocket_outgoing_queue_capacity? : Int, websocket_overflow_policy? :
    NativeWebSocketOverflowPolicy
    , websocket_read_timeout_ms? : Int, websocket_max_message_bytes? : Int) -> NativeServeOptions raise

    Validate and construct NativeServeOptions from optional tuning parameters. Each parameter defaults to None (moon fmt infers that from the ? markers) and is individually range-checked; raises if any value is out of range.

    ResourceConfig

    Configuration for registering a RESTful resource with standard CRUD routes.

    ResourceConfig::ResourceConfig

    Creates a new ResourceConfig with optional CRUD handlers.

    StaticAssetMeta

    pub(all) struct StaticAssetMeta {
    asset_type : String?
    etag : String?
    mtime : Int64?
    path : String?
    size : Int64?
    encoding : String?
    }

    Metadata describing a static asset including its content type, ETag, modification time, and size.

    StaticAssetMeta::StaticAssetMeta

    fn StaticAssetMeta::StaticAssetMeta(asset_type? : String, etag? : String, mtime? : Int64, path? : String, size? : Int64, encoding? : String) -> StaticAssetMeta

    Creates a new StaticAssetMeta with optional fields for asset type, ETag, mtime, path, size, and encoding.

    StaticResolvedResponder

    type StaticResolvedResponder

    StaticResolvedResponder::options

    StaticResolvedResponder::output

    StaticResolvedResponder::output_bytes

    fn StaticResolvedResponder::output_bytes(self : StaticResolvedResponder) -> Bytes?

    html

    Creates an HTML responder from any Show value, setting the Content-Type to text/html.

    text

    Creates a plain-text responder from any Show value, setting the Content-Type to text/plain.