Sign in

    moonapi

    moonapi — a typed web framework for MoonBit (\u2190 FastAPI): routing, typed extractors, descriptor-driven validation, multi-version OpenAPI/Swagger (2.0 / 3.0 / 3.1) with security schemes, dependency injection, OAuth2 password-bearer with scopes, per-operation security enforcement, multipart and urlencoded form extractors, response_model filtering, background tasks, sub-application mounting, a CORS/gzip/exception-handler middleware stack with per-status handlers, Server-Sent Events, and WebSocket routes, on the moonasgi SEAM.

    fastapi
    web
    framework
    openapi
    swagger
    asgi
    moonbit
    Download zip
    Version
    0.16.0
    License
    Apache-2.0
    Last updated
    3 hours ago
    Downloads
    266

    #moonapi

    A typed web framework for MoonBit — ← FastAPI.

    Check and Test License mooncakes

    Moved on mooncakes from Lfan-ke/moonapi to moonbitstack/moonapi.

    moonapi builds an AsgiApp from typed routes and generates its own OpenAPI / Swagger document — the role FastAPI plays for Python. It depends only on moonasgi, so it's backend-agnostic (routing and OpenAPI run in-process on every backend); a server such as mooncat runs the resulting app.

    flowchart LR routes["typed routes<br/>App::get / post / …"] --> app["**moonapi** App"] app -->|"App::to_asgi()"| asgi(["moonasgi AsgiApp"]) app -->|"App::openapi()"| spec["OpenAPI 2.0 / 3.0 / 3.1"] asgi --> cat["mooncat serves it"]

    #Quickstart

    Routing and the OpenAPI document run on every backend. Serving needs a socket, so the entry point is async fn main and the target is --target native:

    async fn main {
    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"))

    // One set of routes, every mainstream spec version.
    println(app.openapi_json(version=OpenApi31)) // OpenAPI 3.1.0
    println(app.openapi_json(version=OpenApi30)) // OpenAPI 3.0.3
    println(app.openapi_json(version=Swagger20)) // Swagger 2.0

    @mooncat.serve(app.to_asgi(), port=12000) catch {
    err => println("stopped: \{err}")
    }
    }

    moon run src/main --target native curl http://127.0.0.1:12000/users/233

    moon.mod needs moonbitstack/moonapi, and — for the serving half — moonbitstack/mooncat plus moonbitlang/async, which async fn main requires by name. The package importing them needs the same three in its moon.pkg. Drop the last two and the routes and the document still build on wasm, wasm-gc and js.

    App::enable_docs() mounts /openapi.json, /docs and /redoc instead of printing the document; swagger_ui() and redoc_ui() return those pages on their own.

    For a runnable integration of the root API, two routers, authentication, validation, middleware, background work, streaming and docs, see the complete application example. The full example list is in examples/README.md. On Windows, macOS or Linux, start the native server with moon run examples/25-full-example --target native. The service provides Swagger UI at http://127.0.0.1:8000/docs, ReDoc at http://127.0.0.1:8000/redoc, and OpenAPI 3.0.3 at http://127.0.0.1:8000/openapi.json.

    #Packages

    Each concern is its own subpackage, so a program links the parts it uses. The root re-exports all of them, so @moonapi.App, @moonapi.text and the rest work as they always did.

    PackageWhat it holdsDepends on
    moonapiApp and its methods, the façadeeverything below
    moonapi/routeMethod, Route, Router, path matchingctx, endpoint, security, socket
    moonapi/openapithe document's info block, routes as operationsroute, schema
    moonapi/securityschemes, guards, per-operation requirements, tokensctx, schema, status
    moonapi/midCORS, gzip, exception handlersctx
    moonapi/endpointParam, ResponseSpec, Endpoint and its validationctx, schema
    moonapi/ctxContext and every extractor, background tasksschema, status
    moonapi/schemaSchema, constraints, ValidationError, of_modelstatus
    moonapi/socketWebSocket connections and routesnothing
    moonapi/statusresponse helpers, cookies, SSE, status constantsnothing
    moonapi/didependency injectionnothing

    Constructors are written without the package. pub using re-exports types but not their constructors, so @moonapi.OpenApi31 does not resolve. Wherever the expected type is known — an argument, a field, a match — the bare name does: version=OpenApi31, Param::new("id", InPath, schema=SInt). The one place it is not known is a raised error, where the subpackage is named: raise @mid.HttpException(...).

    #What's here (v0)

    • Routing — App::get/post/put/patch/delete/route, :param path segments extracted into Context::param, correct 404 (no path) vs 405 (path but not method). A route takes the operation arguments FastAPI's path operations take — summary, description, tags, deprecated, operation_id, status_code, responses, name, include_in_schema, and an openapi_extra fragment merged over the generated operation object — and App::url_for(name, params) resolves a named route back to its path (← url_path_for), mount prefix included.
    • Routers — Router collects routes away from any application, and App::include_router(router, prefix=, tags=, security=, dependencies=, responses=, deprecated=, include_in_schema=) folds them in as the app's own (← FastAPI's APIRouter / include_router). The arguments given at the join reach every route in the group: tags, security requirements and dependencies lead the route's own, group responses are documented under it, and deprecated / include_in_schema mark or hide the whole set. A router is a value, so the same one can be included twice under different prefixes.
    • Descriptor tree — a runtime Schema / Param / Endpoint tree (a one-first-class-value substitute for FastAPI's from-signature reflection) that a typed route carries. Walked once to (a) emit complete OpenAPI request/response body schemas — objects, arrays, scalars, required, nullable (type: [t, "null"] in 3.1, nullable in 3.0, x-nullable in 2.0), a Map[String, T] as additionalProperties, Any as the empty schema, a format such as binary, and defaults, with named models hoisted under components/schemas and referenced by $ref — and (b) drive request validation off the same tree. A Param carries the same constraints a Field does, plus a default and an alias, so a query parameter's bounds are both documented and enforced, and a 422 points at the name the client actually sent. User structs describe themselves with derive(ToJson) + a T::schema() associated function + a one-line ToSchema bridge (the mctl-friendly shape).
    • Multi-version OpenAPI — App::openapi / openapi_json emit Swagger 2.0, OpenAPI 3.0.3, and OpenAPI 3.1.0 from the same routes and descriptors (3.x requestBody + components/schemas; 2.0 body-parameter + definitions), because a good FastAPI is not pinned to one spec version.
    • Typed body extractors — Context::body[T] deserialises the JSON body into a derive(FromJson) struct; Context::body_validated[T] first checks it against the endpoint descriptor and returns either the built value or a FastAPI-shaped 422 error list — schema emission, validation, and deserialisation all off one descriptor.
    • Dependency injection — a Container (provider registry + dependency_overrides) with request-scoped, one-shot resolution (per-request caching) and yield-style teardown run LIFO around the handler — the explicit MoonBit equivalent of FastAPI's Depends. App::depends(container.erase()) attaches one to the application, and any route builder takes dependencies=["key"] (← dependencies=[Depends(...)]): the keys resolve before the handler and are torn down after it, whatever the handler did. The teardown is handed the error that ended the request, so cleanup can tell a rollback from a commit.
    • OAuth2 + JWT security — a /token password-grant endpoint issues an HS256 JWT (create_access_token), and an OAuth2PasswordBearer reads the Authorization: Bearer header, verifies the token, and enforces scopes: 401 on a missing or invalid/expired token, 403 when a valid token lacks a required scope. The token itself is mooncred's and the algorithms are mooncrypt's; moonapi names HS256 and hands over a key. alg: "none" cannot be expressed, the verifier's algorithm is the one that counts rather than the token's, and signatures compare in constant time — all of that is mooncred's to guarantee, and it does.
    • Form & file extractors — Context::form parses both an application/x-www-form-urlencoded body (percent- and +-decoded) and a multipart/form-data body, splitting the boundary stream into fields and byte-exact uploads (filename + content-type + size + the part's own headers + raw bytes). Reading the body is moonhttp/form's, over moonmime and moonurl; a body is attacker-controlled and already buffered, so @moonmime.Limits bounds what one may spend — a thousand parts of a megabyte by default — and a body over either bound comes back as None, refused whole rather than truncated. Context::oauth2_password_form reads the OAuth2 password form off it.
    • response_model — filter_response / json_model validate a handler's return value against a declared Schema and project it down to exactly the model's fields, so a route can hold a richer object internally than it exposes (an id, a password hash) and still emit only what it promised.
    • Middleware & exception handlers — an outer middleware chain (App::middleware) with cors(...) (preflight + actual-request headers, configurable origins/methods/headers, credentials, exposed headers) and gzip(...) (moonzip's compressor), plus exception handlers (App::exception_handler): a handler raises an HttpException and the app maps it to a response, falling through to a built-in {"detail": ...} for HttpException and a 500 for anything else. App::add_status_handler(code, ...) swaps in a custom response for any error status — a routing 404 / 405 or a raised exception's status — so an app can serve its own error pages.
    • Per-operation security — a route carries security=[SecurityRequirement::new(scheme, scopes=[...])], and every scheme shape has a guard: secure_oauth2, secure_oauth2_code, secure_bearer, secure_api_key (header / query / cookie), secure_basic, secure_digest, secure_openid. Each surfaces the scheme in the spec and registers the check that runs before the handler — 401 with the WWW-Authenticate challenge the scheme owes (a scoped bearer route names the scope it wanted), 403 on a missing scope or a rejected API key. Every one takes description= and auto_error= (with the flag off, the guard admits an anonymous caller and leaves the decision to the route). A bare add_security_scheme documents without enforcing (FastAPI's split between a scheme and a wired dependency).
    • Background tasks — a background-aware route (App::route_bg) receives a BackgroundTasks queue; add_task defers work that the app runs, in order, after the response is sent (← FastAPI's BackgroundTasks), so a slow write never delays the client.
    • Sub-application mounting — App::mount(prefix, subapp) composes routers: a request under prefix is routed by the sub-app with the prefix stripped (its own middleware, security, and background tasks apply), and the sub-app's routes and security schemes fold into the parent's merged OpenAPI document under the prefix. Mounts nest. App::mount_handler(prefix, handler) mounts a foreign moonasgi Handler the same way — a third-party component, or static files — which the app routes to but does not document.
    • Streaming responses — App::stream(path, handler) / App::route_stream(verb, ...) register a route returning a moonasgi.StreamingResponse, whose chunks reach the client as separate body events — a client reads the first long before the last one exists. App::handle_with_stream is the chunk-level view a test reads. A middleware is typed buffered-in, buffered-out, so one that rewrites the body (gzip) collapses the reply to a single chunk rather than cutting new bytes at boundaries that no longer describe them.
    • Server-Sent Events — moonhttp/sse frames an event per the WHATWG event-stream format (id / event / retry / multi-line data / : comments); sse_response is a text/event-stream stream of those frames, one chunk per event, so each dispatches on arrival. Hand it to App::stream. (An event stream delivered as one body is not an event stream — it is a file shaped like one.)
    • WebSocket routes — App::websocket(path, handler) over the moonasgi WS SEAM. The handler drives a WebSocket (accept / receive / send / close); it's a synchronous core, so drive_websocket runs it against an in-memory frame queue in a test and App::to_asgi serves it over the async transport.
    • Security schemes — the six OpenAPI shapes (OAuth2 password and authorization-code flows, HTTP bearer / basic / digest, an API key in a header / query / cookie, and OpenID Connect) are emitted in each dialect's form: components/securitySchemes in 3.x, securityDefinitions in 2.0, where an http scheme becomes the standard apiKey-in-Authorization workaround and OpenID Connect — which 2.0 cannot express — is left out rather than described as something it is not.
    • Documentation UI — App::enable_docs() mounts /openapi.json, /docs and /redoc, all three with include_in_schema=false so turning documentation on does not change the document. swagger_ui() and redoc_ui() return the pages alone, for an app that serves them under its own paths or behind its own guard.
    • Lifespan hooks — App::on_startup / App::on_shutdown (← FastAPI's lifespan), driven over the ASGI lifespan protocol. Startup in registration order, shutdown in reverse, so a resource is released before whatever opened it. App::lifespan_handler runs the same thing in-process, for a test with no socket.
    • Responses — text, html, json and json_model helpers over moonasgi.Response, plus redirect(url, status=307) (← RedirectResponse; the URL is encoded over the characters a URI reserves for structure, so an already-encoded URL passes through and a smuggled CRLF cannot open a header) and file_response(content, filename=, media_type=, inline=) (← FileResponse: a media type guessed from the extension, Content-Length, and a Content-Disposition that falls back to RFC 6266's filename* when the name will not survive quoting). It takes bytes rather than a path because moonapi has no filesystem — the same app runs on wasm, js and native.
    • Conditional and partial serving — Context::serve(content, ...) is file_response with the request weighed: 304 when the client's copy is current, 412 when a precondition fails, 206 for a range (one Content-Range, or multipart/byteranges for several), 416 for a range that is not there. This is what makes a download resumable. The entity-tags, the five precondition fields and the range arithmetic are moonhttp's conditional and range, so it is RFC 9110 §13 and §14 rather than a guess. Everything turns on etag=; left out, one is hashed from the content, which costs a pass over the body — pass @conditional.stamp(length~, modified~) for one stat's worth instead.
    • Data URLs — data_response(url) serves what a data: URL carries, under the media type it names; data_url(content, media_type=) writes one. This is what canvas.toDataURL() and FileReader.readAsDataURL() hand back, and how a client posts an image inside a JSON field. Not a data URL yields None, so a handler tells "no picture" from "not a picture" without touching base64.
    • Cookies — set_cookie(resp, name, value, max_age=, expires=, path=, domain=, secure=, http_only=, same_site=) and delete_cookie(...) (← response.set_cookie / delete_cookie). Each returns a new response carrying one more Set-Cookie, which is the correct wire shape: two cookies are two headers, never one folded field. expires is a @moondate.Moment, because an expiry is a moment and the format says how to write one; delete_cookie expires by both Max-Age=0 and the epoch. Context::cookie reads them back. The cookie itself — both headers, each read and written, and the stripping of the octets RFC 6265 forbids so a value cannot forge an attribute or open a header of its own — is moonhttp's cookie package, which a client uses from the other side.
    • Status constants — the 63 HTTP_* and 15 WS_* names FastAPI re-exports from Starlette (HTTP_404_NOT_FOUND, WS_1008_POLICY_VIOLATION), so a route table shows a deliberate 307 rather than a bare number.
    • One description, several uses — Schema::of_model(model, known=) takes a moonmodel description and gives back a schema, so the same declaration that produces the SQL columns in moonorm and the SDL in moongql drives validation and the OpenAPI document here. Written once, read four ways. A Ref is looked up in known; one that is missing, or that leads back into a model already being written, comes back in the second return value with the reason rather than being guessed at — a schema is a finite value, so a cycle cannot be one. Nothing else changes: the hand-written Schema tree is still there and still the shorter way to say something small.

    Verified across all backends (wasm, wasm-gc, js, native) in CI, 0 warnings under --deny-warn.

    #Configuration

    Nothing here decides for you twice. Every bound is an argument with a published default, and every default is the one the mainstream framework uses.

    ctx.query("tag") // the preset bound
    ctx.query("tag", limits=@moonurl.Limits::new(pairs=100000))
    ctx.form(limits=@moonmime.Limits::new(part_size=8 << 20)) // an upload endpoint

    @moonapi.create_access_token(sub, secret, now, extra={ "tenant": Json::string("acme") })
    @moonapi.sse_response(events, headers=[("cache-control", "no-store")])
    @moonapi.sse_response(events, space=false)

    SettingDefaultWhy that one
    query_limits.pairs1000qs, and therefore Express, allows a thousand query parameters
    query_limits.size64 KiBWell past the 8 KiB the common servers allow a whole request line
    form's limits@moonmime.limitsStarlette's max_files and max_fields, with python-multipart's part size
    expires_in_secs3600The hour every OAuth2 example issues
    space on a streamonThe space after a colon is universal on the wire and stripped by every reader

    #Where a setting can arrive twice

    create_access_token computes sub, iat, exp and scopes from its arguments, and extra adds claims alongside them. sse_response sets the three headers a stream needs, and headers adds its own. Where the two name the same thing, the rule is published rather than implied:

    FunctionWho wins by defaultWhat happens when both speak
    create_access_tokenthe argumentsaborts — a token whose subject is not the subject passed in is a mistake in the program
    sse_responsethe caller's headerreplaces ours, so a response never carries two content-type headers

    Both take wins to turn the direction around and clash to choose between merging in silence, aborting, and handing the decision to a callback of your own. They are the same wins and clash mooncred uses, with the same meanings.

    #Design notes

    Two places make an explicit, documented trade-off rather than a silent shortcut:

    • GZip is moonzip's, whose output zlib reads and whose reader takes what zlib writes. What lives here is the middleware around it: when to compress, and which headers to set.
    • WebSocket handlers are a synchronous core: the same handler runs in a test and under a server. Because the core can't suspend on the async transport (MoonBit runs async only in an async context), the serving shell buffers the client's inbound frames, runs the handler, then emits its frames. Content and order are preserved — exact for echo, broadcast, and request-reply — but it doesn't interleave live per-frame with the client.

    #Roadmap (transliterating FastAPI)

    The descriptor tree (Endpoint / Param / Schema) drives OpenAPI body schemas and validation; on top sit the typed derive(FromJson) body extractors, the dependency-injection container, the OAuth2 password-bearer security layer, the Form / File extractors, and response_model filtering. On the middleware side: CORS, gzip, exception and per-status handlers, Server-Sent Events, and WebSocket routes.

    0.9.0 is where moonapi stopped being several libraries at once. The cryptography went to mooncrypt, the token format to mooncred, the wire formats to moonhttp, the compression to moonzip, and the JSON to moonjson — 2 329 lines out of the framework and into libraries anything can use, with every existing test still passing. What is left is routing, extraction, validation, OpenAPI, injection and middleware, which is what a web framework is.

    0.13.1 finished the file story. Context::serve weighs the conditional fields and the Range before answering, so a download resumes and a repeat request costs a header rather than a body. The tags, the fields and the arithmetic are moonhttp's; the four answers and the envelope are here.

    Next: splitting what remains into packages, so a program that wants the router does not link the OpenAPI writer; static files and templates; and codegen'd request schemas via moonctl.

    #License

    Apache-2.0.

    ApiHandler

    A moonapi route handler: request context in, response out. It may raise an HttpException (or any error) instead of returning — the app catches it and maps it to a response through the registered exception handlers, so handlers read like FastAPI's raise HTTPException(...) rather than threading a Result back by hand. A plain non-raising closure is still a valid handler.

    ApiInfo

    What the document says about the API itself — FastAPI's info block plus servers. An app carries one (App::describe sets it) so that every emission of the document, including the route enable_docs registers, agrees.

    ApiKey

    An API-key scheme: the parameter name the key arrives under, where it arrives, and the predicate that accepts a presented key. FastAPI's APIKeyHeader only extracts and leaves the check to a dependency; a route guard needs both, so the check travels with the scheme.

    ApiKeyIn

    Where an API key travels (← APIKeyHeader / APIKeyQuery / APIKeyCookie).

    AuthenticatedUser

    The identity a verified token carries, injected into a protected handler (← the object FastAPI's get_current_user returns). subject is the sub claim, scopes the granted scopes, and claims the whole verified payload for anything else the handler needs (exp, custom claims).

    BackgroundHandler

    A background-aware handler: it additionally receives the request's BackgroundTasks queue, so it can schedule work to run after its response is sent (← declaring a BackgroundTasks parameter in FastAPI).

    BackgroundTasks

    A queue of deferred thunks (← FastAPI's BackgroundTasks). A background-aware route receives one per request, calls add_task to enqueue work, and the app runs the queue — in enqueue order — after the response has been sent.

    BasicAuth

    An HTTP Basic scheme (← FastAPI's HTTPBasic): the realm named in the challenge, and the predicate that accepts a presented username/password.

    Constraint

    A value constraint on a field (JSON-Schema keyword ↔ Pydantic argument).

    Contact

    Build the OpenAPI / Swagger document for the app as a Json value, walking the registered routes once into paths → methods → operations. OpenAPI contact info (← FastAPI's contact): every field optional, emitted only when non-empty.

    Container

    The provider registry: key -> Provider, plus a separate overrides map that shadows it. Overrides are FastAPI's app.dependency_overrides — a test swaps a real dependency (a live DB session) for a fake without touching the routes. A registered override always wins over the base provider.

    Context

    The per-request context handed to a handler: the raw request plus the path parameters extracted from the matched route (:name segments).

    CorsConfig

    CORS policy (← Starlette's CORSMiddleware). Origins, methods, and headers are allow-lists; the *_all flags open a dimension wholesale. Per the Fetch standard, allow_credentials forbids the * wildcard in the reflected Access-Control-Allow-Origin, so with credentials the request origin is echoed back instead.

    Deps

    A container with its dependency value type erased (Container::erase builds one), so a non-generic App can hold one. Resolving through it runs a provider's factory and records its teardown exactly as Scope::get does; what it cannot do is hand the value back — which is precisely what a route-level dependency does not need, since FastAPI's dependencies=[...] discards the values it builds and keeps only their effects.

    DigestAuth

    An HTTP Digest scheme (← FastAPI's HTTPDigest, which likewise carries the credential and no more): verify decides whether the Digest parameter string a client sent is acceptable. Computing the RFC 7616 response digest is the application's, since only it holds the password store.

    Endpoint

    The runtime endpoint descriptor a route can carry — the one first-class value that replaces FastAPI reading a handler's signature. Walked once for the OpenAPI operation (parameters + requestBody + responses, with every named object hoisted into components/schemas) and for request validation.

    ExceptionHandler

    An exception handler: given the request context and the raised error, return Some(response) to handle it or None to defer to the next handler. The explicit MoonBit form of FastAPI's @app.exception_handler(ExcType) — the None case stands in for "this handler isn't registered for that type".

    Field

    One field of an object schema: its name, its schema, whether it is required, an optional description, its value constraints, and the default that stands in when a body leaves it out.

    HttpBasicCredentials

    The credentials carried in an HTTP Basic Authorization header (← FastAPI's HTTPBasicCredentials): the username and password from base64(username:password).

    HttpException

    An HTTP error a handler can raise to short-circuit with a status and body (← FastAPI's HTTPException). detail is any JSON (a string is the common case); headers are added to the response (e.g. a WWW-Authenticate challenge). Caught by the app and mapped to a response.

    License

    OpenAPI license info (← FastAPI's license_info): name required, url optional.

    Method

    HTTP methods a moonapi route can bind to.

    OAuth2CodeBearer

    An OAuth2 authorization-code bearer scheme (← FastAPI's OAuth2AuthorizationCodeBearer): the authorization_url a browser is sent to, the token_url the code is exchanged at, an optional refresh_url, and the shared HS256 secret a presented token is verified against.

    OAuth2PasswordBearer

    An OAuth2 password-bearer security scheme (← FastAPI's OAuth2PasswordBearer). Holds the token_url (the endpoint that issues tokens, surfaced to API docs) and the shared HS256 secret used to verify presented tokens.

    OAuth2PasswordRequestForm

    The parsed OAuth2 password-grant form (← FastAPI's OAuth2PasswordRequestForm). The token endpoint reads username / password to authenticate and scopes (the space-delimited scope field, split into a list) to stamp into the issued token. grant_type is "password" for this flow; client_id / client_secret are optional confidential-client credentials.

    ObjectSchema

    The body of an object schema: its component name (empty = an anonymous inline object; non-empty = hoisted to components/schemas and referenced), its ordered fields, and an optional description.

    OpenApiVersion

    Target OpenAPI / Swagger document version. moonapi emits every mainstream version from the same route descriptors — a "good FastAPI" is not pinned to one spec version.

    Param

    A single request parameter descriptor: its name, location, scalar schema, whether it is required, an optional description, its value constraints, the default that stands in when the request omits it, and the alias it travels under on the wire. Path parameters are always required (OpenAPI requires it); the constructor keeps the caller's value but validation treats path params as mandatory.

    ParamLoc

    Where a parameter is carried, the OpenAPI in locations: the path, the query string, a header, or a cookie.

    Provider

    A provider: a keyed factory that builds a request-scoped dependency value, with an optional teardown run after the handler (FastAPI's yield dependencies, whose post-yield body is cleanup). The factory runs at most once per request scope; the teardown receives the produced value and the error that ended the request, None when it succeeded — the exception a FastAPI yield dependency sees when it wraps its yield in a try. The factory is handed the Scope so it can resolve sub-dependencies through it — FastAPI's Depends(a) where a itself declares Depends(b).

    ResponseSpec

    A single response descriptor: the HTTP status, a human description, and an optional body schema (None for an empty body, e.g. 204).

    Router

    A collection of routes built away from any application and folded into one with App::include_router (← FastAPI's APIRouter). It carries the registration surface of an App and nothing else: middleware, mounts, security schemes, documentation and lifespan belong to the application that includes it.

    Schema

    A JSON-Schema type descriptor — the runtime, first-class value that stands in for FastAPI's from-signature reflection. One Schema tree is walked once to (a) emit a complete OpenAPI request/response body schema — objects, arrays and scalars, with required, and named objects hoisted under components/schemas and referenced by $ref — and (b) drive validation of an inbound JSON value. This is the explicit, MoonBit-idiomatic equivalent of pydantic's type-driven magic (cf. Rust serde + macros, Go struct tags + codegen). A named object carries its fields inline, so the same tree is fully self-describing for validation; the name is used only to deduplicate it into components on emit.

    Scope

    A request-scoped resolution scope. Each dependency is built at most once and its value cached for the life of the scope (FastAPI's per-request dependency cache), and each built value's teardown is recorded to run — in reverse registration order (LIFO) — when the scope closes. Open one per request, resolve dependencies through it, then close it (or use Container::run).

    SecurityRequirement

    A security requirement on a route: the scheme name (which must match a name declared with App::add_security_scheme / App::secure_oauth2) and the scopes the caller must hold. Emitted as one {scheme: [scopes]} entry of the operation's OpenAPI security array.

    SecurityScheme

    A security scheme describing how a client authenticates. Mirrors the OpenAPI scheme types: the two OAuth2 flows a browser or a service actually uses (password and authorization code), HTTP bearer / basic / digest, an API key in a header / query / cookie, and OpenID Connect discovery.

    SecurityScopes

    The scopes a route required at the point its security guard runs (← FastAPI's SecurityScopes). A guard reads them to check what the caller was granted and to build the WWW-Authenticate challenge a 401 owes the client.

    Server

    A servers entry (← FastAPI's servers): a base URL and an optional description.

    StreamHandler

    A streaming route's handler: request context in, a chunked response out (← returning a StreamingResponse from a FastAPI path operation). Every chunk becomes one body event on the wire, so a client reads the first long before the last one exists — the point of streaming, and what a single buffered Response cannot express.

    ToSchema

    The impl-able version of a type's descriptor. A user struct implements it — the value is ignored; it exists so descriptor-carrying code can be generic over "a type that knows its own schema". The primary, mctl-friendly shape is still a plain associated function T::schema() -> Schema (no instance needed, mirroring FastAPI referencing the model class); this trait bridges to it.

    Unresolved

    A reference that could not be written into the schema, and why.

    Reported rather than dropped or guessed at: a body schema silently missing a field is a validation hole that shows up as accepted-but-wrong data.

    ValidationError

    One entry in a 422 response's detail array, mirroring FastAPI / pydantic v2: where the error is (loc, e.g. ["query", "q"]), a human msg, and a machine kind (serialised as the JSON key type).

    WebSocket

    The handler's view of a WebSocket connection. It reads client frames off an inbound queue and records its own actions (accept / send / close) into an outbound event log the transport replays. params are the matched :name path segments, as with an HTTP Context.

    WsHandler

    A WebSocket route handler: given the connection, drive the exchange. Usually accept, then a receive loop, then close.

    WsMessage

    One inbound WebSocket message: a text frame or a binary frame.

    App

    A moonapi application: routes, WebSocket routes, an outer middleware chain, exception handlers, mounted sub-applications, the security schemes surfaced in the OpenAPI document (with optional runtime enforcers), per-status exception handlers, and the verification clock. Compiles to a moonasgi AsgiApp any server (mooncat) can run.

    App::add_security_scheme

    fn App::add_security_scheme(self : App, name : String, scheme :
    SecurityScheme
    , description? : String) -> Unit

    Declare a security scheme under name, surfaced in the emitted OpenAPI document (components/securitySchemes in 3.x, securityDefinitions in Swagger 2.0) so the generated spec describes how to authenticate. description is the prose shown beside it in the documentation UI.

    This declares only. A route naming this scheme in its security is documented as protected and left unguarded, exactly as a FastAPI scheme that no path operation depends on is — the secure_* functions are the ones that wire an enforcer.

    App::add_status_handler

    fn App::add_status_handler(self : App, status : Int, handler : (
    Context
    ) ->
    Response
    ) -> Unit

    Register a per-status exception handler (← FastAPI's add_exception_handler keyed by an HTTP status code). Whenever an error path yields status — a routing 404 / 405, or a raised HttpException (or the fallback 500) — the handler's response replaces the default, so an app can serve a custom error page. Successful handler returns are never rewritten.

    App::delete

    fn App::delete(self : App, path : String, handler : (
    Context
    ) ->
    Response
    raise, summary? : String, description? : String, tags? : Array[String], deprecated? : Bool, operation_id? : String, status_code? : Int, responses? : Array[
    ResponseSpec
    ], name? : String, endpoint? :
    Endpoint
    , security? : Array[
    SecurityRequirement
    ], dependencies? : Array[String], include_in_schema? : Bool, validate? : Bool, openapi_extra? : Json) -> Unit

    Register a DELETE route.

    App::depends

    fn App::depends(self : App, deps :
    Deps
    ) -> Unit

    Give the app the dependency container its routes resolve their declared dependencies through (← FastAPI's Depends wiring, which a route names and the application supplies). The container's value type is erased on the way in, because a route-level dependency runs for its effect and its value is never handed to the handler — the same thing FastAPI's dependencies=[...] does with the values it builds.

    A route that declares a dependency the container does not provide is answered with a 500: the setup the route promised did not happen, and running the handler as though it had is worse than saying so.

    App::describe

    fn App::describe(self : App, title? : String, api_version? : String, description? : String, terms_of_service? : String, contact? :
    Contact
    ?, license? :
    License
    ?, servers? : Array[
    Server
    ]) -> Unit

    Set what the OpenAPI document says about this API (← FastAPI's FastAPI(title=…,description=…, contact=…, license_info=…, servers=…)). App::openapi and the /openapi.json route enable_docs registers both read it, so the document a client fetches and the one a test builds cannot describe different APIs.

    App::enable_docs

    fn App::enable_docs(self : App, openapi_url? : String?, docs_url? : String?, redoc_url? : String?, version? :
    OpenApiVersion
    ) -> Unit

    Serve the app's own documentation, the way FastAPI does out of the box: the OpenAPI document at openapi_url, Swagger UI at docs_url, and ReDoc at redoc_url. Pass None for any of the three to leave it off. The three routes are kept out of the document they serve, so enabling docs does not change the spec a client reads.

    It is a call rather than a default because registering routes behind the app's back would surprise anyone mounting this app under a prefix.

    App::exception_handler

    Register an exception handler. On a raised error the handlers are tried in registration order; the first to return Some(response) wins. An unhandled error falls through to the built-in mapping — an HttpException becomes its own status / detail, anything else a 500.

    App::get

    fn App::get(self : App, path : String, handler : (
    Context
    ) ->
    Response
    raise, summary? : String, description? : String, tags? : Array[String], deprecated? : Bool, operation_id? : String, status_code? : Int, responses? : Array[
    ResponseSpec
    ], name? : String, endpoint? :
    Endpoint
    , security? : Array[
    SecurityRequirement
    ], dependencies? : Array[String], include_in_schema? : Bool, validate? : Bool, openapi_extra? : Json) -> Unit

    Register a GET route. A shorthand for route(Get, ...) carrying the same documentation and security arguments.

    App::handle

    Route a request to its handler through the middleware chain, returning 404 when no path matches and 405 when a path matches but no method does. Any error a handler raises is mapped to a response by the exception handlers. Background tasks a handler scheduled are dropped on this path; use handle_with_background (as to_asgi does) to run them.

    App::handle_with_background

    Route a request through the middleware chain and return both the response and the background queue the handler filled — the caller (to_asgi, or a test) runs the queue after the response is sent. handle is the plain-response wrapper over this.

    App::handle_with_stream

    Route a request through the middleware chain and return the reply in its streamed form, plus the background queue the handler filled. to_asgi sends each chunk as its own body event; a test reads chunks to see where the boundaries fell. A route that is not a streaming one comes back as a single chunk, so this answers every request, not only the streamed ones.

    A middleware is typed buffered-in, buffered-out, so the chain is run over the joined body and the chunks are kept only when what came back is what went in. A middleware that rewrote the body — gzip — has produced something the old boundaries no longer describe, and cutting the new bytes at them would send a corrupt stream.

    App::include_router

    fn App::include_router(self : App, router :
    Router
    , prefix? : String, tags? : Array[String], security? : Array[
    SecurityRequirement
    ], dependencies? : Array[String], responses? : Array[
    ResponseSpec
    ], deprecated? : Bool, include_in_schema? : Bool) -> Unit

    Fold router's routes into this app (FastAPI's include_router, and the same name — include is a reserved word). Each is registered under prefix and becomes one of the app's own routes, and the arguments given here reach every one of them:

    • tags, security and dependencies are placed before the route's own, so a group's tag leads and a group-wide requirement or dependency cannot be dropped by a route that adds one of its own;
    • responses are documented under the route's, which therefore wins any status both name;
    • deprecated marks the whole group, and include_in_schema=false hides it, neither of which a route can undo.

    App::lifespan_handler

    This app's hooks as a moonasgi LifespanHandler — the synchronous core, which is what makes boot and teardown testable on every backend without a server. Mounted apps are included: a mount is part of the composition being started, and nothing else would ever drive its hooks.

    App::middleware

    Add an outer middleware. Middlewares wrap the router as an onion; the first registered is the outermost (it sees the request first and the response last). cors(...) and gzip(...) are middlewares.

    App::mount

    fn App::mount(self : App, prefix : String, subapp : App) -> Unit

    Mount a sub-application under prefix (← FastAPI's app.mount(prefix, sub)). A request whose path lies under prefix is routed by subapp with the prefix stripped (its own middleware, security, and background tasks apply), and the sub-app's routes appear under prefix in the merged OpenAPI document with its security schemes folded into the parent's.

    App::mount_handler

    fn App::mount_handler(self : App, prefix : String, handler : (
    Request
    ) ->
    Response
    ) -> Unit

    Mount a foreign moonasgi handler under prefix (← FastAPI mounting a plain ASGI app, app.mount("/static", StaticFiles(...))). A request under prefix is handed to handler with the prefix stripped, exactly as for a sub-app.

    A handler is not a moonapi application, so it contributes nothing to the OpenAPI document and has no lifespan of its own to run — the app only routes to it. Mounts are tried in registration order, whichever kind they are.

    App::new

    fn App::new() -> App

    Create an empty application. The verification clock defaults to 0 (Unix epoch); a server sets a real one with App::set_clock, and tests inject a fixed time so token expiry is deterministic.

    App::on_shutdown

    fn App::on_shutdown(self : App, hook : () -> Unit raise) -> Unit

    Run hook when the server shuts down, after the last request has been served (← FastAPI's on_event("shutdown")). Hooks run in reverse registration order, so a resource is released before whatever it was opened from. A hook that raises does not stop the others — shutdown reports the first failure once the rest have run.

    App::on_startup

    fn App::on_startup(self : App, hook : () -> Unit raise) -> Unit

    Run hook when the server starts, before it accepts the first request (← FastAPI's on_event("startup")). Hooks run in the order they were registered; one that raises aborts startup, and the server is told why.

    App::openapi

    fn App::openapi(self : App, version? :
    OpenApiVersion
    , title? : String, api_version? : String, description? : String, terms_of_service? : String, contact? :
    Contact
    ?, license? :
    License
    ?, servers? : Array[
    Server
    ]) -> Json

    Build the app's OpenAPI document as a Json value, walking the registered routes — this app's and every mounted sub-app's, each under its prefix — into paths, methods and operations. version picks the dialect: Swagger 2.0, OpenAPI 3.0.3 or 3.1.0 off the identical routes. Routes registered with include_in_schema=false are left out.

    App::openapi_json

    fn App::openapi_json(self : App, version? :
    OpenApiVersion
    ) -> String

    The app's OpenAPI / Swagger document for version, stringified.

    App::patch

    fn App::patch(self : App, path : String, handler : (
    Context
    ) ->
    Response
    raise, summary? : String, description? : String, tags? : Array[String], deprecated? : Bool, operation_id? : String, status_code? : Int, responses? : Array[
    ResponseSpec
    ], name? : String, endpoint? :
    Endpoint
    , security? : Array[
    SecurityRequirement
    ], dependencies? : Array[String], include_in_schema? : Bool, validate? : Bool, openapi_extra? : Json) -> Unit

    Register a PATCH route: change part of the addressed resource.

    App::post

    fn App::post(self : App, path : String, handler : (
    Context
    ) ->
    Response
    raise, summary? : String, description? : String, tags? : Array[String], deprecated? : Bool, operation_id? : String, status_code? : Int, responses? : Array[
    ResponseSpec
    ], name? : String, endpoint? :
    Endpoint
    , security? : Array[
    SecurityRequirement
    ], dependencies? : Array[String], include_in_schema? : Bool, validate? : Bool, openapi_extra? : Json) -> Unit

    Register a POST route — the verb that carries a request body, so this is the one most often given an endpoint describing that body.

    App::put

    fn App::put(self : App, path : String, handler : (
    Context
    ) ->
    Response
    raise, summary? : String, description? : String, tags? : Array[String], deprecated? : Bool, operation_id? : String, status_code? : Int, responses? : Array[
    ResponseSpec
    ], name? : String, endpoint? :
    Endpoint
    , security? : Array[
    SecurityRequirement
    ], dependencies? : Array[String], include_in_schema? : Bool, validate? : Bool, openapi_extra? : Json) -> Unit

    Register a PUT route: replace the addressed resource wholesale.

    App::route

    fn App::route(self : App, verb :
    Method
    , path : String, handler : (
    Context
    ) ->
    Response
    raise, summary? : String, description? : String, tags? : Array[String], deprecated? : Bool, operation_id? : String, status_code? : Int, responses? : Array[
    ResponseSpec
    ], name? : String, endpoint? :
    Endpoint
    , security? : Array[
    SecurityRequirement
    ], dependencies? : Array[String], include_in_schema? : Bool, validate? : Bool, openapi_extra? : Json) -> Unit

    Register a route for an explicit method. An optional endpoint descriptor makes the route fully typed (its parameters, request body, and responses surface in the OpenAPI document and drive validation); security attaches per-operation requirements (emitted as OpenAPI security and enforced before the handler when their scheme has an enforcer); dependencies names provider keys resolved through the app's container (App::depends) before the handler and torn down after it, whatever the handler did.

    The rest describe the operation: summary and description are its prose, operation_id the stable handle client generators name their method after, status_code the status its success response is documented under, responses further documented responses, name the handle App::url_for resolves, and openapi_extra a fragment merged over the generated operation object — the same keyword arguments FastAPI's path operations take.

    App::route_bg

    fn App::route_bg(self : App, verb :
    Method
    , path : String, handler : (
    Context
    ,
    BackgroundTasks
    ) ->
    Response
    raise, summary? : String, description? : String, tags? : Array[String], deprecated? : Bool, operation_id? : String, status_code? : Int, responses? : Array[
    ResponseSpec
    ], name? : String, endpoint? :
    Endpoint
    , security? : Array[
    SecurityRequirement
    ], dependencies? : Array[String], include_in_schema? : Bool, validate? : Bool, openapi_extra? : Json) -> Unit

    Register a background-aware route: the handler additionally receives the request's BackgroundTasks queue, whose thunks the app runs after the response is sent (← declaring a BackgroundTasks parameter in FastAPI).

    App::route_stream

    fn App::route_stream(self : App, verb :
    Method
    , path : String, handler : (
    Context
    ) ->
    StreamingResponse
    raise, summary? : String, description? : String, tags? : Array[String], deprecated? : Bool, operation_id? : String, status_code? : Int, responses? : Array[
    ResponseSpec
    ], name? : String, endpoint? :
    Endpoint
    , security? : Array[
    SecurityRequirement
    ], dependencies? : Array[String], include_in_schema? : Bool, validate? : Bool, openapi_extra? : Json) -> Unit

    Register a streaming route (← a FastAPI path operation returning a StreamingResponse). The handler's chunks reach the client as separate body events, so a long or open-ended reply starts arriving before it is finished.

    A stream is only streamed as far as the app can honestly keep it one: moonasgi types a middleware as buffered response in, buffered response out, so a middleware that rewrites the body — gzip does — collapses the reply to a single chunk. Everything else about the route is ordinary; it documents, validates and enforces security exactly as route does.

    App::secure_api_key

    fn App::secure_api_key(self : App, name : String, key :
    ApiKey
    , description? : String, auto_error? : Bool) -> Unit

    Declare an API-key scheme under name and wire it as a runtime enforcer (← FastAPI's Security(APIKeyHeader(name=...))). The scheme appears in the document as apiKey in the header, query or cookie the key names, and a route requiring name is refused with 403 unless it presents a key the scheme accepts.

    App::secure_basic

    fn App::secure_basic(self : App, name : String, basic :
    BasicAuth
    , description? : String, auto_error? : Bool) -> Unit

    Declare an HTTP Basic scheme under name and wire it as a runtime enforcer (← Security(HTTPBasic())). A route requiring name is refused with 401 and a Basic challenge unless it presents credentials the scheme accepts.

    App::secure_bearer

    fn App::secure_bearer(self : App, name : String, secret : String, bearer_format? : String, description? : String, auto_error? : Bool) -> Unit

    Declare a plain HTTP bearer scheme under name and wire it as a runtime enforcer (← Security(HTTPBearer())). The token is verified as an HS256 JWT against secret at the app clock's time, and the route's required scopes are checked against the token's — the same guard the OAuth2 flows use, without an OAuth2 flow to advertise.

    App::secure_digest

    fn App::secure_digest(self : App, name : String, digest :
    DigestAuth
    , description? : String, auto_error? : Bool) -> Unit

    Declare an HTTP Digest scheme under name and wire it as a runtime enforcer (← Security(HTTPDigest())). A route requiring name is refused with 403 unless it presents a Digest credential the scheme accepts.

    App::secure_oauth2

    fn App::secure_oauth2(self : App, name : String, bearer :
    OAuth2PasswordBearer
    , scopes? : Array[(String, String)], description? : String, auto_error? : Bool) -> Unit

    Declare an OAuth2 password-bearer scheme under name and wire it as a runtime enforcer. Like add_security_scheme it surfaces the scheme in the OpenAPI document (with the advertised scopes and description), and additionally registers the guard a route names in its security: the app pulls the bearer token, verifies it against bearer's secret at the app clock's time, and checks the route's required scopes — returning 401/403 before the handler runs.

    auto_error=false admits the request instead of refusing it (← the argument every FastAPI security class takes, whose dependency then yields None and leaves the decision to the route).

    App::secure_oauth2_code

    fn App::secure_oauth2_code(self : App, name : String, code :
    OAuth2CodeBearer
    , scopes? : Array[(String, String)], description? : String, auto_error? : Bool) -> Unit

    Declare an OAuth2 authorization-code scheme under name and wire it as a runtime enforcer (← Security(OAuth2AuthorizationCodeBearer(...))). The document describes the browser redirect and token endpoints and the advertised scopes; the guard verifies the presented bearer token exactly as the password flow's does, since by the time a request arrives the two flows have produced the same credential.

    App::secure_openid

    fn App::secure_openid(self : App, name : String, url : String, secret : String, description? : String, auto_error? : Bool) -> Unit

    Declare an OpenID Connect scheme under name and wire it as a runtime enforcer (← Security(OpenIdConnect(openIdConnectUrl=...))). url is the discovery document a client reads every other parameter from; secret is what the ID token presented as a bearer credential is verified against.

    Swagger 2.0 has no openIdConnect type, so this scheme is absent from a 2.0 document rather than described as something it is not.

    App::set_clock

    fn App::set_clock(self : App, clock : () -> Int64) -> Unit

    Set the clock the app reads to verify token expiry when enforcing route security (Unix seconds). A native server passes the wall clock; a test passes a fixed function so expiry is deterministic.

    App::stream

    fn App::stream(self : App, path : String, handler : (
    Context
    ) ->
    StreamingResponse
    raise, summary? : String, description? : String, tags? : Array[String], deprecated? : Bool, operation_id? : String, status_code? : Int, responses? : Array[
    ResponseSpec
    ], name? : String, endpoint? :
    Endpoint
    , security? : Array[
    SecurityRequirement
    ], dependencies? : Array[String], include_in_schema? : Bool, validate? : Bool, openapi_extra? : Json) -> Unit

    Register a streaming GET route — the verb a stream is nearly always read over, and what an SSE endpoint is. A shorthand for route_stream(Get, ...).

    App::to_asgi

    Compile the app to a moonasgi AsgiApp a server can run: drain the request body, route it, and stream the response back over the SEAM.

    App::url_for

    fn App::url_for(self : App, name : String, params? : Map[String, String]) -> String?

    The path of the route registered under name, with its :name segments filled from params (← FastAPI's url_path_for). Mounted sub-applications are searched too, so what comes back already carries the mount prefix — the path a client would call, not the one the sub-app knows itself by.

    None when nothing is registered under that name, or when params is missing one the path needs. Values are substituted as given: a value with a / in it lands as extra path segments, so encode before calling if that matters.

    App::websocket

    fn App::websocket(self : App, path : String, handler : (
    WebSocket
    ) -> Unit) -> Unit

    Register a WebSocket route (← FastAPI's @app.websocket(path)). The path matches with the same :name segment rules as HTTP routes.

    Mount

    type Mount

    What a mount prefix routes to: a moonapi sub-application, whose routes, security schemes and lifespan hooks fold into the parent's, or a foreign moonasgi handler, which the parent can only forward requests to.

    HTTP_100_CONTINUE

    let HTTP_100_CONTINUE : Int

    Continue: the headers are acceptable, send the body.

    HTTP_101_SWITCHING_PROTOCOLS

    let HTTP_101_SWITCHING_PROTOCOLS : Int

    Switching Protocols: the server is changing to the protocol Upgrade asked for.

    HTTP_102_PROCESSING

    let HTTP_102_PROCESSING : Int

    Processing: WebDAV, the request is under way and the reply will follow.

    HTTP_103_EARLY_HINTS

    let HTTP_103_EARLY_HINTS : Int

    Early Hints: preload links sent ahead of the real response.

    HTTP_200_OK

    let HTTP_200_OK : Int

    OK.

    HTTP_201_CREATED

    let HTTP_201_CREATED : Int

    Created: the request made a new resource, named in Location.

    HTTP_202_ACCEPTED

    let HTTP_202_ACCEPTED : Int

    Accepted: taken for processing, outcome not yet known.

    HTTP_203_NON_AUTHORITATIVE_INFORMATION

    let HTTP_203_NON_AUTHORITATIVE_INFORMATION : Int

    Non-Authoritative Information: an intermediary altered the origin's payload.

    HTTP_204_NO_CONTENT

    let HTTP_204_NO_CONTENT : Int

    No Content: succeeded, and there is nothing to send back.

    HTTP_205_RESET_CONTENT

    let HTTP_205_RESET_CONTENT : Int

    Reset Content: succeeded; the client should clear the form that sent it.

    HTTP_206_PARTIAL_CONTENT

    let HTTP_206_PARTIAL_CONTENT : Int

    Partial Content: the byte ranges Range asked for.

    HTTP_207_MULTI_STATUS

    let HTTP_207_MULTI_STATUS : Int

    Multi-Status: WebDAV, one status per member of a collection.

    HTTP_208_ALREADY_REPORTED

    let HTTP_208_ALREADY_REPORTED : Int

    Already Reported: WebDAV, this member was enumerated earlier in the reply.

    HTTP_226_IM_USED

    let HTTP_226_IM_USED : Int

    IM Used: the body is the result of applying delta encodings.

    HTTP_300_MULTIPLE_CHOICES

    let HTTP_300_MULTIPLE_CHOICES : Int

    Multiple Choices: several representations, pick one.

    HTTP_301_MOVED_PERMANENTLY

    let HTTP_301_MOVED_PERMANENTLY : Int

    Moved Permanently: use Location from now on.

    HTTP_302_FOUND

    let HTTP_302_FOUND : Int

    Found: a temporary move. Clients rewrite POST to GET here, which is why 307 exists.

    HTTP_303_SEE_OTHER

    let HTTP_303_SEE_OTHER : Int

    See Other: fetch the outcome with a GET at Location — the redirect after a form post.

    HTTP_304_NOT_MODIFIED

    let HTTP_304_NOT_MODIFIED : Int

    Not Modified: the client's cached copy is still current.

    HTTP_305_USE_PROXY

    let HTTP_305_USE_PROXY : Int

    Use Proxy: deprecated by RFC 9110.

    HTTP_306_RESERVED

    let HTTP_306_RESERVED : Int

    Reserved: never standardised, and reserved so nothing else claims it.

    HTTP_307_TEMPORARY_REDIRECT

    let HTTP_307_TEMPORARY_REDIRECT : Int

    Temporary Redirect: like 302, but the method and body must be kept.

    HTTP_308_PERMANENT_REDIRECT

    let HTTP_308_PERMANENT_REDIRECT : Int

    Permanent Redirect: like 301, but the method and body must be kept.

    HTTP_400_BAD_REQUEST

    let HTTP_400_BAD_REQUEST : Int

    Bad Request: malformed enough that the server will not act on it.

    HTTP_401_UNAUTHORIZED

    let HTTP_401_UNAUTHORIZED : Int

    Unauthorized: not authenticated. The reply must carry WWW-Authenticate.

    HTTP_402_PAYMENT_REQUIRED

    let HTTP_402_PAYMENT_REQUIRED : Int

    Payment Required: reserved.

    HTTP_403_FORBIDDEN

    let HTTP_403_FORBIDDEN : Int

    Forbidden: authenticated, and still not allowed.

    HTTP_404_NOT_FOUND

    let HTTP_404_NOT_FOUND : Int

    Not Found.

    HTTP_405_METHOD_NOT_ALLOWED

    let HTTP_405_METHOD_NOT_ALLOWED : Int

    Method Not Allowed: the path exists under another verb, listed in Allow.

    HTTP_406_NOT_ACCEPTABLE

    let HTTP_406_NOT_ACCEPTABLE : Int

    Not Acceptable: nothing on offer matches the request's Accept.

    HTTP_407_PROXY_AUTHENTICATION_REQUIRED

    let HTTP_407_PROXY_AUTHENTICATION_REQUIRED : Int

    Proxy Authentication Required.

    HTTP_408_REQUEST_TIMEOUT

    let HTTP_408_REQUEST_TIMEOUT : Int

    Request Timeout: the client took too long to send it.

    HTTP_409_CONFLICT

    let HTTP_409_CONFLICT : Int

    Conflict: it clashes with the resource's current state.

    HTTP_410_GONE

    let HTTP_410_GONE : Int

    Gone: it existed and was removed on purpose.

    HTTP_411_LENGTH_REQUIRED

    let HTTP_411_LENGTH_REQUIRED : Int

    Length Required: Content-Length is missing and this server insists on it.

    HTTP_412_PRECONDITION_FAILED

    let HTTP_412_PRECONDITION_FAILED : Int

    Precondition Failed: an If-* header did not hold.

    HTTP_413_REQUEST_ENTITY_TOO_LARGE

    let HTTP_413_REQUEST_ENTITY_TOO_LARGE : Int

    Content Too Large — Starlette's name keeps RFC 7231's "Request Entity Too Large".

    HTTP_414_REQUEST_URI_TOO_LONG

    let HTTP_414_REQUEST_URI_TOO_LONG : Int

    URI Too Long — Starlette's name keeps RFC 7231's "Request-URI Too Long".

    HTTP_415_UNSUPPORTED_MEDIA_TYPE

    let HTTP_415_UNSUPPORTED_MEDIA_TYPE : Int

    Unsupported Media Type: the body's Content-Type is one this route cannot read.

    HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE

    let HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE : Int

    Range Not Satisfiable: no part of the requested range exists.

    HTTP_417_EXPECTATION_FAILED

    let HTTP_417_EXPECTATION_FAILED : Int

    Expectation Failed: the Expect header cannot be met.

    HTTP_418_IM_A_TEAPOT

    let HTTP_418_IM_A_TEAPOT : Int

    I'm a Teapot: RFC 2324's joke, kept because clients test for it.

    HTTP_421_MISDIRECTED_REQUEST

    let HTTP_421_MISDIRECTED_REQUEST : Int

    Misdirected Request: this connection is not authoritative for that authority.

    HTTP_422_UNPROCESSABLE_ENTITY

    let HTTP_422_UNPROCESSABLE_ENTITY : Int

    Unprocessable Content: well-formed, and it fails validation — what unprocessable returns.

    HTTP_423_LOCKED

    let HTTP_423_LOCKED : Int

    Locked: WebDAV, the resource is locked.

    HTTP_424_FAILED_DEPENDENCY

    let HTTP_424_FAILED_DEPENDENCY : Int

    Failed Dependency: WebDAV, a request this one depended on failed.

    HTTP_425_TOO_EARLY

    let HTTP_425_TOO_EARLY : Int

    Too Early: replaying this early-data request could be a replay attack.

    HTTP_426_UPGRADE_REQUIRED

    let HTTP_426_UPGRADE_REQUIRED : Int

    Upgrade Required: resend over the protocol named in Upgrade.

    HTTP_428_PRECONDITION_REQUIRED

    let HTTP_428_PRECONDITION_REQUIRED : Int

    Precondition Required: the server refuses to act on an unconditional write.

    HTTP_429_TOO_MANY_REQUESTS

    let HTTP_429_TOO_MANY_REQUESTS : Int

    Too Many Requests: rate limited; Retry-After says when to come back.

    HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE

    let HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE : Int

    Request Header Fields Too Large.
    let HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS : Int

    Unavailable For Legal Reasons.

    HTTP_500_INTERNAL_SERVER_ERROR

    let HTTP_500_INTERNAL_SERVER_ERROR : Int

    Internal Server Error: the fallback for an error the app did not map.

    HTTP_501_NOT_IMPLEMENTED

    let HTTP_501_NOT_IMPLEMENTED : Int

    Not Implemented: the server does not support the method at all.

    HTTP_502_BAD_GATEWAY

    let HTTP_502_BAD_GATEWAY : Int

    Bad Gateway: an upstream sent something invalid.

    HTTP_503_SERVICE_UNAVAILABLE

    let HTTP_503_SERVICE_UNAVAILABLE : Int

    Service Unavailable: down or overloaded, and expected back.

    HTTP_504_GATEWAY_TIMEOUT

    let HTTP_504_GATEWAY_TIMEOUT : Int

    Gateway Timeout: an upstream did not answer in time.

    HTTP_505_HTTP_VERSION_NOT_SUPPORTED

    let HTTP_505_HTTP_VERSION_NOT_SUPPORTED : Int

    HTTP Version Not Supported.

    HTTP_506_VARIANT_ALSO_NEGOTIATES

    let HTTP_506_VARIANT_ALSO_NEGOTIATES : Int

    Variant Also Negotiates: the negotiation is configured in a circle.

    HTTP_507_INSUFFICIENT_STORAGE

    let HTTP_507_INSUFFICIENT_STORAGE : Int

    Insufficient Storage: WebDAV, no room to store the representation.

    HTTP_508_LOOP_DETECTED

    let HTTP_508_LOOP_DETECTED : Int

    Loop Detected: WebDAV, the traversal is cyclic.

    HTTP_510_NOT_EXTENDED

    let HTTP_510_NOT_EXTENDED : Int

    Not Extended.

    HTTP_511_NETWORK_AUTHENTICATION_REQUIRED

    let HTTP_511_NETWORK_AUTHENTICATION_REQUIRED : Int

    Network Authentication Required: a captive portal wants a login first.

    WS_1000_NORMAL_CLOSURE

    let WS_1000_NORMAL_CLOSURE : Int

    Normal Closure: the purpose the connection was opened for is fulfilled.

    WS_1001_GOING_AWAY

    let WS_1001_GOING_AWAY : Int

    Going Away: the peer is shutting down or navigating off the page.

    WS_1002_PROTOCOL_ERROR

    let WS_1002_PROTOCOL_ERROR : Int

    Protocol Error.

    WS_1003_UNSUPPORTED_DATA

    let WS_1003_UNSUPPORTED_DATA : Int

    Unsupported Data: a frame of a type this endpoint cannot accept.

    WS_1005_NO_STATUS_RCVD

    let WS_1005_NO_STATUS_RCVD : Int

    No Status Received: reported locally when a close frame carried no code.

    WS_1006_ABNORMAL_CLOSURE

    let WS_1006_ABNORMAL_CLOSURE : Int

    Abnormal Closure: reported locally when the connection died without a close frame.

    WS_1007_INVALID_FRAME_PAYLOAD_DATA

    let WS_1007_INVALID_FRAME_PAYLOAD_DATA : Int

    Invalid Frame Payload Data: a text frame that is not valid UTF-8, say.

    WS_1008_POLICY_VIOLATION

    let WS_1008_POLICY_VIOLATION : Int

    Policy Violation: the generic refusal when no other code fits.

    WS_1009_MESSAGE_TOO_BIG

    let WS_1009_MESSAGE_TOO_BIG : Int

    Message Too Big.

    WS_1010_MANDATORY_EXT

    let WS_1010_MANDATORY_EXT : Int

    Mandatory Extension: the client required an extension the server did not negotiate.

    WS_1011_INTERNAL_ERROR

    let WS_1011_INTERNAL_ERROR : Int

    Internal Error: the server hit an unexpected condition.

    WS_1012_SERVICE_RESTART

    let WS_1012_SERVICE_RESTART : Int

    Service Restart.

    WS_1013_TRY_AGAIN_LATER

    let WS_1013_TRY_AGAIN_LATER : Int

    Try Again Later: overloaded; come back.

    WS_1014_BAD_GATEWAY

    let WS_1014_BAD_GATEWAY : Int

    Bad Gateway: the gateway got an invalid response upstream.

    WS_1015_TLS_HANDSHAKE

    let WS_1015_TLS_HANDSHAKE : Int

    TLS Handshake: reported locally when the handshake failed.

    check_constraints

    Enforce the constraints on an inbound value, appending a Pydantic-shaped ValidationError (located at loc) for each violation. A constraint that does not apply to the value's kind (a length bound on a number, say) is simply skipped, as pydantic does.

    cors

    fn cors(allow_origins? : Array[String], allow_all_origins? : Bool, allow_methods? : Array[String], allow_headers? : Array[String], allow_all_headers? : Bool, allow_credentials? : Bool, expose_headers? : Array[String], max_age? : Int) -> (((
    Request
    ) ->
    Response
    ) -> ((
    Request
    ) ->
    Response
    ))

    A CORS middleware for the given policy. It answers preflight OPTIONS requests (those carrying Access-Control-Request-Method) directly with a 204 and the negotiated Access-Control-* headers, and decorates every other cross-origin response with Access-Control-Allow-Origin (plus Vary: Origin, exposed headers, and the credentials flag). A request with no Origin, or one from a disallowed origin, passes through untouched.

    create_access_token

    fn create_access_token(subject : String, secret : String, now_secs : Int64, scopes? : Array[String], expires_in_secs? : Int64, extra? : Map[String, Json], wins? :
    Wins
    , clash? :
    OnClash
    [Map[String, Json]]) -> String

    Mint an HS256 access token for subject (← FastAPI's create_access_token). Stamps sub, iat (= now_secs), exp (= now_secs + expires_in_secs), and, when non-empty, scopes; extra merges in any further claims. Times are Unix seconds. secret is the shared HS256 key.

    extra adds claims. Where it names one the arguments already set — sub, iat, exp, scopes — the arguments win and saying it twice aborts, because a token whose subject is not the subject passed in is a mistake in the program rather than a choice about configuration. wins=Extra hands the decision to extra, and clash can silence it or take a callback.

    data_response

    fn data_response(url : String, filename? : String, status? : Int, inline? : Bool) ->
    Response
    ?

    Serve what a data: URL carries (RFC 2397), under the media type it names — what canvas.toDataURL() and FileReader.readAsDataURL() hand back.

    None when it is not a data URL, so a handler tells "no picture" from "not a picture" without touching base64.

    data_url

    fn data_url(content : Bytes, media_type? : String) -> String

    The content as a data: URL, for embedding it instead of costing a second request.
    fn delete_cookie(resp :
    Response
    , name : String, path? : String?, domain? : String, secure? : Bool, http_only? : Bool, same_site? :
    SameSite
    ?) ->
    Response

    Expire the cookie named name (← FastAPI's response.delete_cookie): an empty value with Max-Age=0 and a date in the past, so a browser that honours only one of the two still drops it.

    A cookie is identified by name, domain and path together, so path and domain must match what set it — otherwise this writes a second, differently scoped cookie and the original survives.

    drive_websocket

    fn drive_websocket(handler : (
    WebSocket
    ) -> Unit, inbound : Array[
    WsMessage
    ], params? : Map[String, String], subprotocols? : Array[String]) -> Array[
    Event
    ]

    Run a WebSocket handler against an in-memory frame queue and return the events it emitted — the synchronous test driver (the WS half of a TestClient). Feed the client's frames as inbound; get back the handler's accept / send / close sequence.

    file_response

    fn file_response(content : Bytes, filename? : String, media_type? : String, status? : Int, inline? : Bool) ->
    Response

    A download (← FastAPI's FileResponse): a media type from filename's extension unless media_type names one, Content-Length, and a Content-Disposition. No filename, no disposition.

    Bytes rather than a path: the same app runs on four backends and only the server can read a file on any of them.

    Always the whole thing. [Context::serve] weighs the request first.

    filter_response

    Validate value against model and, if it conforms, return it filtered down to the model's declared fields (extras dropped, nested objects and arrays projected too). On a mismatch return the located ValidationErrors under ["response"] — the same shape request validation produces. This is FastAPI's response_model: the outgoing shape is the model, not whatever the handler happened to build.

    gzip

    A GZip middleware: responses at least min_size bytes are re-encoded as gzip when the client sent Accept-Encoding: gzip and the response isn't already content-encoded. Sets Content-Encoding: gzip, updates Content-Length, and adds Vary: Accept-Encoding.

    The gzip stream is a complete RFC 1952 container — correct header, CRC-32, and ISIZE — around a real RFC 1951 DEFLATE payload: LZ77 back-references coded with the fixed Huffman table (deflate.mbt), so the body actually shrinks. Dynamic Huffman would tighten the ratio further and is the documented next step.

    html

    fn html(status : Int, body : String) ->
    Response

    An HTML response — what the documentation pages are served as.

    http_error

    fn http_error(status : Int, detail : String, headers? : Array[(String, String)]) ->
    HttpException

    Build an HttpException with a string detail — the common case — and optional extra headers. raise http_error(404, "Item not found") reads like FastAPI's raise HTTPException(404, "Item not found").

    json

    fn json(status : Int, value : Json) ->
    Response

    A JSON response serialised from a Json value.

    json_model

    A JSON response whose body is value filtered through model. On success a status response carrying only the model's declared fields; on a model mismatch a 500 whose body lists the response-validation errors — the return value didn't match what the route promised, which is a server-side fault.

    parse_basic_auth

    Parse an HTTP Basic Authorization header value into credentials (← FastAPI's HTTPBasic), or None. Matches the Basic scheme case-insensitively (RFC 7617), base64-decodes the rest, and splits on the first colon so a password may itself contain colons.

    query_limits

    How much of a query string is a query string.

    A thousand parameters is what qs allows Express by default, and far past anything a link carries; sixty-four kilobytes for one value is well past the eight the common servers allow a whole request line. Beyond either, the request is an attempt to make the server allocate.

    An endpoint that genuinely takes more says so: ctx.query("tag", limits=@moonurl.Limits::new(pairs=100000)).

    redirect

    fn redirect(url : String, status? : Int) ->
    Response

    A redirect to url (← FastAPI's RedirectResponse).

    307 keeps the method and body, which 302 historically does not. Use 303 after a write, 301 / 308 for a permanent move.

    The URL is escaped as a whole URI reference, so an encoded one is not encoded twice and a \r\n cannot open a header of its own.

    redoc_ui

    fn redoc_ui(spec_url? : String, title? : String) -> String

    A self-contained ReDoc page rendering the document served at spec_url — the second reading of the same spec FastAPI serves at /redoc, three-panel and built for reading rather than for trying calls out.

    schema_of

    The schema of any ToSchema type, without needing a value of it materialised at the call site beyond the one handed in — the generic entry point.
    fn set_cookie(resp :
    Response
    , name : String, value : String, max_age? : Int, expires? :
    Moment
    , path? : String?, domain? : String, secure? : Bool, http_only? : Bool, same_site? :
    SameSite
    ?) ->
    Response

    Add a Set-Cookie header to resp (← FastAPI's response.set_cookie), returning the response that carries it; the original is untouched, so a handler can hand the same base response to two callers.

    max_age is the lifetime in seconds and expires the moment it dies; giving neither makes it a session cookie. path defaults to /, and None omits the attribute so the cookie scopes to the request's own directory. http_only keeps it away from scripts, secure keeps it off plaintext connections, and same_site defaults to Lax — the browser default, and the one that stops a cross-site form post from carrying a session.

    sse_response

    fn sse_response(events : Array[
    Event
    ], status? : Int, headers? : Array[(String, String)], space? : Bool, wins? :
    Wins
    , clash? :
    OnClash
    [Array[(String, String)]]) ->
    StreamingResponse

    A text/event-stream response, one frame to a chunk.

    A chunk per frame is the whole point: a stream that arrives as one body is not a stream, it is a file shaped like one. A client dispatches an event when it reads that event's blank line, so the frames have to reach it separately for anything to happen before the last one is written.

    Framing an event is moonhttp/sse's job. What belongs here is the response around the frames, and the three headers that stop an intermediary buffering or closing the stream.

    The three headers a stream needs are set here. One named in headers too replaces ours rather than joining it, because a response carrying two content-type headers is not a response with a choice in it. wins=Base keeps ours, and clash can abort or take a callback instead — worth setting when overriding content-type, which stops the stream being a stream.

    swagger_ui

    fn swagger_ui(spec_url? : String, title? : String) -> String

    A self-contained Swagger UI page rendering the document served at spec_url.

    text

    fn text(status : Int, body : String) ->
    Response

    A plain-text response.

    token_response

    fn token_response(access_token : String) ->
    Response

    The 200 token response body {"access_token": ..., "token_type": "bearer"} — the OAuth2 password-grant reply FastAPI's token endpoint returns.

    unprocessable

    A 422 Unprocessable Entity response whose application/json body lists the validation errors, exactly as FastAPI reports a failed request.

    validate_schema

    Validate a JSON value against the descriptor schema, appending FastAPI-shaped errors to errs (located at loc). This is what "the descriptor drives validation" means: the very tree that emits the OpenAPI body schema also decides whether an inbound body conforms — one source of truth, exactly as pydantic derives both from one model. A named object is validated against its inline fields, so no $ref resolution is needed here.

    validation_error_body

    The {"detail": [ ... ]} body FastAPI returns when request validation fails, built from a list of ValidationErrors.

    with_constraints

    Merge the constraints into a scalar/array schema object for OpenAPI emission. exclusiveMinimum / exclusiveMaximum are numeric under OpenAPI 3.1 (JSON-Schema 2020-12) but a boolean flag alongside minimum / maximum under Swagger 2.0 and OpenAPI 3.0, so the form is version-aware.