moonapi

A typed web framework for MoonBit — FastAPI-style routing and multi-version OpenAPI, on the moonasgi SEAM. Backend-agnostic — the async transport lives in the server (mooncat) that runs the app.

CItestsGitHublicense
$moon add moonbitstack/moonapi

✶ The contract at a glance

let app = App::new()
app.get("/users/:id", ctx => text(200, "user " + ctx.param("id").unwrap()))

let spec = app.openapi_json(version=OpenApi31)   // also OpenApi30 / Swagger20
@mooncat.serve(app.to_asgi(), port=8000)         // run it (native)

§Application & routing

The App, its route builders (get / post / ...), :param path matching, the Context, and text / json response helpers - plus App::to_asgi to run anywhere.

enum
enum Method

HTTP methods a moonapi route can bind to.

struct
struct Context

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

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

Look up a path parameter by name.

type
type ApiHandler = (Context) -> @moonasgi.Response raise

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.

type
type BackgroundHandler = (Context, BackgroundTasks) -> @moonasgi.Response raise

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

type
type StreamHandler = (Context) -> @moonasgi.StreamingResponse raise

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.

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

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

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

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

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

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

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

fn
fn App::middleware(self : App, mw : @moonasgi.Middleware) -> Unit

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.

fn
fn App::exception_handler(self : App, h : ExceptionHandler) -> Unit

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.

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

fn
fn App::route( self : App, verb : Method, path : String, handler : ApiHandler, summary? : String = "", description? : String = "", tags? : Array[String] = [], deprecated? : Bool = false, operation_id? : String = "", status_code? : Int, responses? : Array[ResponseSpec] = [], name? : String = "", endpoint? : Endpoint, security? : Array[SecurityRequirement] = [], dependencies? : Array[String] = [], include_in_schema? : Bool = true, validate? : Bool = true, 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.

fn
fn App::route_bg( self : App, verb : Method, path : String, handler : BackgroundHandler, summary? : String = "", description? : String = "", tags? : Array[String] = [], deprecated? : Bool = false, operation_id? : String = "", status_code? : Int, responses? : Array[ResponseSpec] = [], name? : String = "", endpoint? : Endpoint, security? : Array[SecurityRequirement] = [], dependencies? : Array[String] = [], include_in_schema? : Bool = true, validate? : Bool = true, 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).

fn
fn App::route_stream( self : App, verb : Method, path : String, handler : StreamHandler, summary? : String = "", description? : String = "", tags? : Array[String] = [], deprecated? : Bool = false, operation_id? : String = "", status_code? : Int, responses? : Array[ResponseSpec] = [], name? : String = "", endpoint? : Endpoint, security? : Array[SecurityRequirement] = [], dependencies? : Array[String] = [], include_in_schema? : Bool = true, validate? : Bool = true, 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.

fn
fn App::stream( self : App, path : String, handler : StreamHandler, summary? : String = "", description? : String = "", tags? : Array[String] = [], deprecated? : Bool = false, operation_id? : String = "", status_code? : Int, responses? : Array[ResponseSpec] = [], name? : String = "", endpoint? : Endpoint, security? : Array[SecurityRequirement] = [], dependencies? : Array[String] = [], include_in_schema? : Bool = true, validate? : Bool = true, 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, ...).

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

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

fn
fn App::enable_docs( self : App, openapi_url? : String? = Some("/openapi.json"), docs_url? : String? = Some("/docs"), redoc_url? : String? = Some("/redoc"), version? : OpenApiVersion = OpenApi31) -> 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.

fn
fn App::post( self : App, path : String, handler : ApiHandler, summary? : String = "", description? : String = "", tags? : Array[String] = [], deprecated? : Bool = false, operation_id? : String = "", status_code? : Int, responses? : Array[ResponseSpec] = [], name? : String = "", endpoint? : Endpoint, security? : Array[SecurityRequirement] = [], dependencies? : Array[String] = [], include_in_schema? : Bool = true, validate? : Bool = true, 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.

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

Register a PUT route: replace the addressed resource wholesale.

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

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

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

Register a DELETE route.

fn
fn App::secure_oauth2( self : App, name : String, bearer : OAuth2PasswordBearer, scopes? : Array[(String, String)] = [], description? : String = "", auto_error? : Bool = true) -> 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).

fn
fn App::add_status_handler( self : App, status : Int, handler : (Context) -> @moonasgi.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.

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

fn
fn App::mount_handler( self : App, prefix : String, handler : @moonasgi.Handler) -> 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.

fn
fn App::url_for( self : App, name : String, params? : Map[String, String] = Map([])) -> 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.

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

A plain-text response.

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

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

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

A JSON response serialised from a Json value.

fn
fn App::handle_with_stream( self : App, request : @moonasgi.Request) -> (@moonasgi.StreamingResponse, BackgroundTasks)

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.

fn
fn App::handle_with_background( self : App, request : @moonasgi.Request) -> (@moonasgi.Response, BackgroundTasks)

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.

fn
fn App::handle( self : App, request : @moonasgi.Request) -> @moonasgi.Response

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.

fn
fn App::lifespan_handler(self : App) -> @moonasgi.LifespanHandler

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.

fn
fn App::to_asgi(self : App) -> @moonasgi.AsgiApp

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.

item
extend Method with Eq::

§Routers & composition

APIRouter and include_router - routes collected away from any application and folded into one later, under a prefix and with the tags, security and responses the whole group shares. Plus mounting a foreign moonasgi handler.

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

fn
fn Router::new() -> Router

An empty router. The prefix and the attributes its routes share are given at App::include_router rather than here, so one router can be included twice — under a second prefix, or on another app with different tags.

fn
fn Router::route( self : Router, verb : Method, path : String, handler : ApiHandler, summary? : String = "", description? : String = "", tags? : Array[String] = [], deprecated? : Bool = false, operation_id? : String = "", status_code? : Int, responses? : Array[ResponseSpec] = [], name? : String = "", endpoint? : Endpoint, security? : Array[SecurityRequirement] = [], dependencies? : Array[String] = [], include_in_schema? : Bool = true, validate? : Bool = true, openapi_extra? : Json) -> Unit

Register a route on the router for an explicit method. Takes what App::route takes and means the same by it; the route reaches an application when the router is included.

fn
fn Router::route_bg( self : Router, verb : Method, path : String, handler : BackgroundHandler, summary? : String = "", description? : String = "", tags? : Array[String] = [], deprecated? : Bool = false, operation_id? : String = "", status_code? : Int, responses? : Array[ResponseSpec] = [], name? : String = "", endpoint? : Endpoint, security? : Array[SecurityRequirement] = [], dependencies? : Array[String] = [], include_in_schema? : Bool = true, validate? : Bool = true, openapi_extra? : Json) -> Unit

Register a background-aware route on the router — App::route_bg, deferred to whichever application includes it.

fn
fn Router::stream( self : Router, path : String, handler : StreamHandler, summary? : String = "", description? : String = "", tags? : Array[String] = [], deprecated? : Bool = false, operation_id? : String = "", status_code? : Int, responses? : Array[ResponseSpec] = [], name? : String = "", endpoint? : Endpoint, security? : Array[SecurityRequirement] = [], dependencies? : Array[String] = [], include_in_schema? : Bool = true, validate? : Bool = true, openapi_extra? : Json) -> Unit

Register a streaming GET route on the router — App::stream, deferred to whichever application includes it.

fn
fn Router::get( self : Router, path : String, handler : ApiHandler, summary? : String = "", description? : String = "", tags? : Array[String] = [], deprecated? : Bool = false, operation_id? : String = "", status_code? : Int, responses? : Array[ResponseSpec] = [], name? : String = "", endpoint? : Endpoint, security? : Array[SecurityRequirement] = [], dependencies? : Array[String] = [], include_in_schema? : Bool = true, validate? : Bool = true, openapi_extra? : Json) -> Unit

Register a GET route on the router.

fn
fn Router::post( self : Router, path : String, handler : ApiHandler, summary? : String = "", description? : String = "", tags? : Array[String] = [], deprecated? : Bool = false, operation_id? : String = "", status_code? : Int, responses? : Array[ResponseSpec] = [], name? : String = "", endpoint? : Endpoint, security? : Array[SecurityRequirement] = [], dependencies? : Array[String] = [], include_in_schema? : Bool = true, validate? : Bool = true, openapi_extra? : Json) -> Unit

Register a POST route on the router.

fn
fn Router::put( self : Router, path : String, handler : ApiHandler, summary? : String = "", description? : String = "", tags? : Array[String] = [], deprecated? : Bool = false, operation_id? : String = "", status_code? : Int, responses? : Array[ResponseSpec] = [], name? : String = "", endpoint? : Endpoint, security? : Array[SecurityRequirement] = [], dependencies? : Array[String] = [], include_in_schema? : Bool = true, validate? : Bool = true, openapi_extra? : Json) -> Unit

Register a PUT route on the router.

fn
fn Router::patch( self : Router, path : String, handler : ApiHandler, summary? : String = "", description? : String = "", tags? : Array[String] = [], deprecated? : Bool = false, operation_id? : String = "", status_code? : Int, responses? : Array[ResponseSpec] = [], name? : String = "", endpoint? : Endpoint, security? : Array[SecurityRequirement] = [], dependencies? : Array[String] = [], include_in_schema? : Bool = true, validate? : Bool = true, openapi_extra? : Json) -> Unit

Register a PATCH route on the router.

fn
fn Router::delete( self : Router, path : String, handler : ApiHandler, summary? : String = "", description? : String = "", tags? : Array[String] = [], deprecated? : Bool = false, operation_id? : String = "", status_code? : Int, responses? : Array[ResponseSpec] = [], name? : String = "", endpoint? : Endpoint, security? : Array[SecurityRequirement] = [], dependencies? : Array[String] = [], include_in_schema? : Bool = true, validate? : Bool = true, openapi_extra? : Json) -> Unit

Register a DELETE route on the router.

fn
fn Router::websocket( self : Router, path : String, handler : WsHandler) -> Unit

Register a WebSocket route on the router (← APIRouter.websocket). It takes the including prefix like any other route; the operation attributes do not apply, since a WebSocket route is not an OpenAPI operation.

fn
fn App::include_router( self : App, router : Router, prefix? : String = "", tags? : Array[String] = [], security? : Array[SecurityRequirement] = [], dependencies? : Array[String] = [], responses? : Array[ResponseSpec] = [], deprecated? : Bool = false, include_in_schema? : Bool = true) -> 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.

§Descriptor tree

The runtime Schema descriptor - the first-class value that stands in for FastAPI's from-signature reflection. Walked once to emit complete OpenAPI body schemas (objects / arrays / scalars, required, $ref'd components) and to drive validation. Includes the ToSchema trait and its builders.

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

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

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

fn
fn Field::new( name : String, schema : Schema, required? : Bool = true, description? : String = "", constraints? : Array[Constraint] = [], default? : Json) -> Field

Build a field. required defaults to true (FastAPI treats a field without a default as required). constraints are the Pydantic-style value constraints (ge/le/min_length/pattern/…) that are both emitted into the field's OpenAPI schema and enforced when validating an inbound value. default is emitted into the schema and makes the field optional, since a value that has one is never missing.

fn
fn Field::is_required(self : Field) -> Bool

Whether a body must carry this field. A field with a default never must — its absence is answered by the default, exactly as a Python default argument is, so it is left out of the emitted required list and not reported missing.

fn
fn Schema::object(name : String, fields : Array[Field]) -> Schema

A named object schema: hoisted to components/schemas under name and referenced by $ref wherever it is used.

fn
fn Schema::array(item : Schema) -> Schema

An array schema whose elements all conform to item.

fn
fn Schema::nullable(inner : Schema) -> Schema

A schema that also admits null (← Optional[T]). Wrapping rather than a flag because the three dialects express it three different ways.

fn
fn Schema::map(value : Schema) -> Schema

An object with no declared fields whose values all conform to value (← dict[str, T]). Schema::map(SAny) is the free-form object.

fn
fn Schema::format(base : Schema, format : String) -> Schema

base refined by an OpenAPI format. The format is documentation: it tells a reader and a client generator what the string holds, and validation still checks only base, which is what pydantic does for a format it has no validator for.

fn
fn Schema::json(document : Json) -> Schema

A schema written as JSON Schema itself (2020-12, which is what OpenAPI 3.1 is). Use it for what the other constructors cannot say; it goes into the document unchanged.

fn
fn Schema::binary() -> Schema

The schema of file content: a string with OpenAPI's binary format — what an upload is described as, and what a multipart file field carries.

item
pub(open) trait 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.

item
fn[T : ToSchema] schema_of(x : T) -> Schema

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.

item
extend Schema with Eq::
item
extend ObjectSchema with Eq::
item
extend Field with Eq::

§Endpoint descriptor

Param / ResponseSpec / Endpoint - the descriptor a typed route carries, walked once into an OpenAPI operation (parameters + requestBody + responses, with named schemas hoisted into components).

enum
enum ParamLoc

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

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

fn
fn Param::new( name : String, loc : ParamLoc, schema? : Schema = SStr, required? : Bool = true, description? : String = "", constraints? : Array[Constraint] = [], default? : Json, alias_? : String = "") -> Param

Build a parameter descriptor. schema defaults to a string and required to true. constraints are the Pydantic-style bounds FastAPI reads off a Query(ge=…, max_length=…) — emitted into the parameter's documented schema *and* enforced on the inbound value. default is emitted and makes the parameter optional, since a value that has one is never missing. alias is the name the parameter travels under when that differs from the one the code calls it (← Query(alias="item-query")).

fn
fn Param::key(self : Param) -> String

The name this parameter travels under: its alias when it has one, else its own name. This is what is read off the request, what the document calls it, and what a validation error's loc points at — all three being the client's view of the parameter.

fn
fn Param::is_required(self : Param) -> Bool

Whether a request must carry this parameter. One with a default never must — its absence is answered by the default, exactly as a Python default argument is.

struct
struct ResponseSpec

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

fn
fn ResponseSpec::new( status : Int, description? : String = "OK", body? : Schema) -> ResponseSpec

Build a response descriptor. description defaults to "OK" and there is no body unless one is given.

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

fn
fn Endpoint::new( params? : Array[Param] = [], request_body? : Schema, request_required? : Bool = true, responses? : Array[ResponseSpec] = []) -> Endpoint

Build an endpoint descriptor. Everything is optional: a bare Endpoint::new() describes an endpoint with no parameters, no body, and (on emit) a default 200 OK response.

item
extend ParamLoc with Eq::
item
extend Param with Eq::
item
extend ResponseSpec with Eq::
item
extend Endpoint with Eq::

§OpenAPI & Swagger

Multi-version OpenAPI / Swagger generation (2.0 / 3.0 / 3.1) from the same routes and descriptors, and a ready-to-serve Swagger UI page.

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

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

struct
struct License

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

struct
struct Server

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

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

fn
fn ApiInfo::new() -> ApiInfo

The defaults an app starts with.

fn
fn App::openapi( self : App, version? : OpenApiVersion = OpenApi31, 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.

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

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

fn
fn redoc_ui( spec_url? : String = "/openapi.json", title? : String = "moonapi") -> 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.

fn
fn swagger_ui( spec_url? : String = "/openapi.json", title? : String = "moonapi") -> String

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

item
extend OpenApiVersion with Eq::

§Extraction & validation

Typed extraction off the Context - query / cookie params, JSON body, single JSON fields - plus descriptor-driven validation and FastAPI-shaped ValidationError values and 422 responses.

let
let query_limits : @mime.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=@mime.Limits::new(parts=100000)).

fn
fn Context::query( self : Context, name : String, limits? : @mime.Limits = query_limits) -> String?

Look up a query-string parameter by name, e.g. ?limit=10&q=cat%20dog. Keys and values are percent/plus-decoded, so q above reads back as cat dog. When a key repeats, the first occurrence wins; use query_all to read every one.

fn
fn Context::query_all( self : Context, name : String, limits? : @mime.Limits = query_limits) -> Array[String]

Every value given for name, in the order they appear — ?tag=a&tag=b reads back as ["a", "b"]. Empty when the key is absent.

fn
fn Context::body_json(self : Context) -> Json?

Parse the request body as JSON, returning None for an empty body or one that does not parse — the total counterpart of FastAPI reading a JSON body.

fn
fn Context::json_field(self : Context, name : String) -> Json?

Pull a single field out of a JSON object body by name, None if the body is absent, not an object, or lacks the field.

fn
fn Context::cookie(self : Context, name : String) -> String?

Look up a cookie by name from the request Cookie header, which is a ; -separated list of key=value pairs. Spaces around a name or a value are the separators'; None if there is no Cookie header or the name is absent.

struct
struct 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).

fn
fn ValidationError::missing(loc : Array[String]) -> ValidationError

The canonical "a required parameter was not supplied" error located at loc, matching FastAPI's {"type": "missing", "msg": "Field required"}.

fn
fn ValidationError::type_error( loc : Array[String], kind : String, msg : String) -> ValidationError

A type/parse error located at loc, e.g. kind = "int_parsing" with the matching pydantic message — the shape FastAPI reports for a value of the wrong type.

fn
fn validation_error_body(errors : Array[ValidationError]) -> Json

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

fn
fn unprocessable(errors : Array[ValidationError]) -> @moonasgi.Response

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

fn
fn validate_schema( schema : Schema, value : Json, loc : Array[String], errs : Array[ValidationError]) -> Unit

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.

fn
fn Endpoint::validate( self : Endpoint, ctx : Context) -> Array[ValidationError]

Validate an inbound request ctx against this endpoint descriptor: every declared parameter (path / query / header / cookie) plus the JSON request body, all off the same descriptor tree that emits the OpenAPI operation. Returns the accumulated errors — an empty array means the request conforms, otherwise pass them to unprocessable for a FastAPI-shaped 422.

§Typed body extractors

Context::body deserialises the JSON body into a derive(FromJson) struct, and Context::body_validated checks it against the endpoint descriptor first - yielding a FastAPI-shaped 422 error list on failure and the built value on success, all off one source of truth.

item
fn[T : @json.FromJson] Context::body(self : Context) -> T?

Deserialise the JSON request body into a user type T, None when the body is absent, is not valid JSON, or does not shape-match T. This is the unchecked, best-effort extractor — the total counterpart of writing item: Item on a FastAPI handler when you don't want the framework's 422. T describes itself with derive(@json.FromJson); the deserialisation is core's, so it stays faithful to the JSON shape without any reflection.

item
fn[T : @json.FromJson] Context::body_validated( self : Context, schema : Schema) -> Result[T, Array[ValidationError]]

The validated typed-body extractor — the faithful equivalent of FastAPI declaring a pydantic model parameter: the body is checked against the endpoint's descriptor schema (the same tree that emits the OpenAPI body schema), and only if it conforms is it deserialised into T. On failure it yields the FastAPI-shaped ValidationError list (located under ["body", ...]), ready for unprocessable; on success it yields the built T. Reusing validate_schema here is the point of the descriptor tree: schema emission, request validation, and typed deserialisation are all driven off one source of truth, exactly as pydantic derives all three from one model. Because validation runs first, @json.from_json is reached only for a shape-conforming value; the final catch keeps the extractor total for the residual cases a scalar schema cannot express (e.g. an out-of-range integer).

§Dependency injection

A container (provider registry + dependency_overrides), request-scoped one-shot resolution with per-request caching, and yield-style teardown run LIFO around the handler - the explicit MoonBit equivalent of FastAPI's Depends.

struct
struct Provider[V]

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

item
fn[V] Provider::new( factory : () -> V, teardown? : (V, Error?) -> Unit = (_v, _e) => ()) -> Provider[V]

Build a leaf provider whose factory needs nothing else. teardown defaults to a no-op — the common "plain value, nothing to release" case.

item
fn[V] Provider::scoped( factory : (Scope[V]) -> V, teardown? : (V, Error?) -> Unit = (_v, _e) => ()) -> Provider[V]

Build a provider whose factory resolves other dependencies through the request Scope it is handed — the sub-dependency case (FastAPI's nested Depends).

struct
struct Container[V]

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.

item
fn[V] Container::new() -> Container[V]

An empty container.

item
fn[V] Container::provide( self : Container[V], key : String, factory : () -> V, teardown? : (V, Error?) -> Unit = (_v, _e) => ()) -> Container[V]

Register a base provider under key (last registration wins), returning the container so registrations can chain.

item
fn[V] Container::provide_using( self : Container[V], key : String, factory : (Scope[V]) -> V, teardown? : (V, Error?) -> Unit = (_v, _e) => ()) -> Container[V]

Register a base provider whose factory resolves sub-dependencies through the request scope it is handed (FastAPI's nested Depends). Otherwise like provide.

item
fn[V] Container::override_( self : Container[V], key : String, factory : () -> V, teardown? : (V, Error?) -> Unit = (_v, _e) => ()) -> Container[V]

Register a dependency override for key — FastAPI's app.dependency_overrides[dep] = fake. Takes precedence over the base provider until cleared.

item
fn[V] Container::clear_override(self : Container[V], key : String) -> Unit

Drop the override for key (no-op if none), restoring the base provider.

item
fn[V] Container::clear_overrides(self : Container[V]) -> Unit

Drop every override — the usual test teardown that returns the container to its production wiring.

struct
struct Scope[V]

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

item
fn[V] Container::open_scope(self : Container[V]) -> Scope[V]

Open a fresh request scope over this container.

item
fn[V] Scope::get(self : Scope[V], key : String) -> V?

Resolve key within this scope: return the already-built instance if the dependency was resolved earlier in the same request; otherwise run its factory once, cache the value, register its teardown, and return it. None when no provider (or override) is registered for key.

item
fn[V] Scope::close(self : Scope[V], failure? : Error) -> Unit

Run every recorded teardown in LIFO order and clear them, so a closed scope is inert. Mirrors FastAPI unwinding yield dependencies in reverse — the last opened is torn down first. failure is the error that ended the request and is handed to every teardown, so cleanup can tell a failed request from a successful one and roll back rather than commit.

item
fn[V] Container::run( self : Container[V], handler : (Scope[V]) -> @moonasgi.Response raise) -> @moonasgi.Response raise

Run handler inside a fresh request scope, then tear the scope down — the setup/teardown pair wrapped around a handler, exactly as a FastAPI yield dependency brackets the request. The handler resolves whatever it needs through the scope; every dependency built during the call is released (LIFO) once it returns, then the response is handed back. A handler that raises is released the same way, and the error reaches the teardowns before it is re-raised for the app's exception handlers to map: cleanup that only ran on the happy path would leak exactly when it matters.

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

item
fn[V] Container::erase(self : Container[V]) -> Deps

This container with its value type erased, ready for App::depends. The providers, the per-request cache, the sub-dependency resolution and the LIFO teardown are all the container's own; erasure hides only the value.

§OAuth2 & bearer security

OAuth2 password-bearer: create_access_token issues a scoped HS256 JWT, and OAuth2PasswordBearer reads the Authorization header, verifies the token, and enforces scopes - 401 on a missing / invalid / expired token, 403 on an insufficient scope. The MoonBit equivalent of FastAPI's Security(...).

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

fn
fn Context::oauth2_password_form( self : Context) -> OAuth2PasswordRequestForm?

Read an OAuth2PasswordRequestForm off the request's urlencoded (or multipart) body. None when neither username nor password is present — the body isn't a password-grant form at all — and equally when the body is too large to be one, since a grant carries six fields and nothing that big is a login.

struct
struct 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).

fn
fn AuthenticatedUser::has_scope( self : AuthenticatedUser, scope : String) -> Bool

Whether this user was granted scope.

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

fn
fn OAuth2PasswordBearer::new( token_url : String, secret : String) -> OAuth2PasswordBearer

Build a password-bearer scheme pointing at the token endpoint at token_url, verifying tokens with secret.

fn
fn Context::bearer_token(self : Context) -> String?

Pull the bearer token out of the Authorization: Bearer <token> header, None if the header is absent or isn't a bearer credential. The scheme name is matched case-insensitively, as RFC 6750 requires.

fn
fn OAuth2PasswordBearer::authenticate( self : OAuth2PasswordBearer, ctx : Context, now_secs : Int64, scopes? : Array[String] = []) -> Result[AuthenticatedUser, @moonasgi.Response]

Authenticate a request against this scheme and enforce scopes (← FastAPI's Security(get_current_user, scopes=[...])). On success returns the AuthenticatedUser; otherwise the response to return: - no bearer token -> 401 {"detail":"Not authenticated"} - malformed / bad-signature / expired / not-yet-valid token -> 401 {"detail":"Could not validate credentials"} - valid token missing a required scope -> 403 {"detail":"Not enough permissions"} Both 401s carry the challenge the required scopes describe — Bearer on its own, or Bearer scope="..." — so a client learns what it should have asked for. now_secs is the verification time (Unix seconds), passed in so the check stays pure and testable on every backend.

fn
fn create_access_token( subject : String, secret : String, now_secs : Int64, scopes? : Array[String] = [], expires_in_secs? : Int64 = 3600, extra? : Map[String, Json] = Map([]), wins? : @jwt.Wins = Base, clash? : @jwt.OnClash[Map[String, Json]] = Panic) -> 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.

fn
fn token_response(access_token : String) -> @moonasgi.Response

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

item
extend OAuth2PasswordRequestForm with Eq::

§OpenAPI security schemes

SecurityScheme objects emitted into the generated spec - OAuth2 password flow, HTTP bearer (JWT), API keys, and Basic - under components/securitySchemes in 3.x and securityDefinitions in Swagger 2.0, so the OAuth2 / JWT layer is described to clients.

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

fn
fn OAuth2PasswordBearer::scheme( self : OAuth2PasswordBearer, scopes? : Array[(String, String)] = []) -> SecurityScheme

Build the security scheme that describes this password-bearer flow (← the object FastAPI derives from OAuth2PasswordBearer). scopes are the (name, description) pairs advertised in the OpenAPI document.

§Per-operation security

A SecurityRequirement attaches a declared scheme (and its scopes) to a route: emitted as the operation's OpenAPI security array, and - when the scheme was registered with an enforcer via App::secure_oauth2 - enforced before the handler (401 unauthenticated, 403 on a missing scope).

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

fn
fn SecurityRequirement::new( scheme : String, scopes? : Array[String] = []) -> SecurityRequirement

Require scheme with the given scopes (default: none — authentication with no scope check). App::get(..., security=[SecurityRequirement::new("OAuth2", scopes=["items"])]) reads like FastAPI's Security(oauth2, scopes=["items"]).

item
extend SecurityRequirement with Eq::

§Background tasks

BackgroundTasks queues thunks a background-aware route schedules; the app runs them, in order, after the response is sent - FastAPI's BackgroundTasks.

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

fn
fn BackgroundTasks::new() -> BackgroundTasks

An empty task queue.

fn
fn BackgroundTasks::add_task( self : BackgroundTasks, task : () -> Unit) -> Unit

Enqueue a thunk to run after the response is sent. Tasks run in the order they were added, each after the previous returns (← BackgroundTasks.add_task).

fn
fn BackgroundTasks::len(self : BackgroundTasks) -> Int

How many tasks are queued — the app checks this to skip the drain when a route scheduled nothing.

fn
fn BackgroundTasks::run(self : BackgroundTasks) -> Unit

Run every queued task in order, then clear the queue. Called by the app once the response has been handed to the transport, so a task's latency never delays the client. Idempotent: a second call runs nothing.

§Middleware & exception handlers

The outer middleware chain: cors (preflight + actual-request headers, configurable origins / methods / headers / credentials), gzip (a real DEFLATE compressor, below), per-status handlers for custom error pages, and exception handlers that map a raised HttpException to a response, with a built-in 500 fallback.

item
suberror 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.

fn
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").

type
type ExceptionHandler = (Context, Error) -> @moonasgi.Response?

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

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

fn
fn cors( allow_origins? : Array[String] = [], allow_all_origins? : Bool = false, allow_methods? : Array[String] = [ "GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", ], allow_headers? : Array[String] = [], allow_all_headers? : Bool = false, allow_credentials? : Bool = false, expose_headers? : Array[String] = [], max_age? : Int = 600) -> @moonasgi.Middleware

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.

fn
fn gzip(min_size? : Int = 500) -> @moonasgi.Middleware

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.

§WebSocket routes

App::websocket over the moonasgi WS SEAM. The handler drives a WebSocket (accept / receive / send / close) as a synchronous core, so drive_websocket runs it against an in-memory frame queue in a test and the serving shell runs it over the async transport.

enum
enum WsMessage

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

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

type
type WsHandler = (WebSocket) -> Unit

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

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

Look up a matched path parameter by name.

fn
fn WebSocket::offered_subprotocols(self : WebSocket) -> Array[String]

The subprotocols the client offered (the Sec-WebSocket-Protocol list).

fn
fn WebSocket::accept( self : WebSocket, subprotocol? : String, headers? : Array[(String, String)] = []) -> Unit

Accept the handshake (← await websocket.accept()), optionally selecting a subprotocol and adding response headers. Idempotent: a second call is a no-op, so accept-once handlers stay simple.

fn
fn WebSocket::receive(self : WebSocket) -> WsMessage?

Pull the next client frame, None once the client has sent them all (the disconnect). The receive a handler loops on.

fn
fn WebSocket::receive_text(self : WebSocket) -> String?

The next client frame as text: Some(s) for a text frame, None on a binary frame or the disconnect (← await websocket.receive_text()).

fn
fn WebSocket::receive_bytes(self : WebSocket) -> Bytes?

The next client frame as bytes: Some(b) for a binary frame, None on a text frame or the disconnect.

fn
fn WebSocket::send_text(self : WebSocket, text : String) -> Unit

Send a text frame to the client (← await websocket.send_text(...)).

fn
fn WebSocket::send_bytes(self : WebSocket, bytes : Bytes) -> Unit

Send a binary frame to the client.

fn
fn WebSocket::close( self : WebSocket, code? : Int = 1000, reason? : String = "") -> Unit

Close the connection with a status code (default 1000, normal closure) and reason. Idempotent.

fn
fn WebSocket::sent(self : WebSocket) -> Array[@moonasgi.Event]

The events the handler emitted, in order — the transcript a test asserts on.

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

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

fn
fn drive_websocket( handler : WsHandler, inbound : Array[WsMessage], params? : Map[String, String] = Map([]), subprotocols? : Array[String] = []) -> Array[@moonasgi.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.

item
extend WsMessage with Eq::

§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 exposes only what it promised - FastAPI's response_model.

fn
fn filter_response( model : Schema, value : Json) -> Result[Json, Array[ValidationError]]

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.

fn
fn json_model( status : Int, model : Schema, value : Json) -> @moonasgi.Response

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.

§Redirects, downloads & conditional serving

The response kinds that are an envelope rather than a body: redirect (the status and an escaped Location), file_response (a media type from the filename, Content-Length, and an RFC 6266 Content-Disposition), and Context::serve, which is the same envelope with the request weighed - 304 when the client's copy is current, 412 when a precondition fails, 206 for a Range and 416 for one that cannot be met. Plus the data: URL both ways.

fn
fn redirect(url : String, status? : Int = 307) -> @moonasgi.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.

fn
fn file_response( content : Bytes, filename? : String = "", media_type? : String = "", status? : Int = 200, inline? : Bool = false) -> @moonasgi.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.

fn
fn data_response( url : String, filename? : String = "", status? : Int = 200, inline? : Bool = true) -> @moonasgi.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.

fn
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
fn Context::serve( self : Context, content : Bytes, filename? : String = "", media_type? : String = "", inline? : Bool = false, etag? : @conditional.Tag, modified? : @moondate.Moment, ranges? : Bool = true) -> @moonasgi.Response

[file_response] with the request weighed first (RFC 9110 §13, §14): * 304 — the client's copy is current. * 412 — a precondition it stated does not hold. * 206 — it asked for part, one Content-Range or a multipart/byteranges. * 416 — it asked for what is not there. 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, or any tag of your own. ranges=false withdraws the offer in Accept-Ranges.

§Status codes

The 63 HTTP_* and 15 WS_* constants FastAPI re-exports from Starlette, so a route reads HTTP_404_NOT_FOUND rather than a bare number.

item
const HTTP_100_CONTINUE : Int = 100 ///| /// Switching Protocols: the server is changing to the protocol `Upgrade` asked for. pub const HTTP_101_SWITCHING_PROTOCOLS : Int = 101 ///| /// Processing: WebDAV, the request is under way and the reply will follow. pub const HTTP_102_PROCESSING : Int = 102 ///| /// Early Hints: preload links sent ahead of the real response. pub const HTTP_103_EARLY_HINTS : Int = 103 ///| /// OK. pub const HTTP_200_OK : Int = 200 ///| /// Created: the request made a new resource, named in `Location`. pub const HTTP_201_CREATED : Int = 201 ///| /// Accepted: taken for processing, outcome not yet known. pub const HTTP_202_ACCEPTED : Int = 202 ///| /// Non-Authoritative Information: an intermediary altered the origin's payload. pub const HTTP_203_NON_AUTHORITATIVE_INFORMATION : Int = 203 ///| /// No Content: succeeded, and there is nothing to send back. pub const HTTP_204_NO_CONTENT : Int = 204 ///| /// Reset Content: succeeded; the client should clear the form that sent it. pub const HTTP_205_RESET_CONTENT : Int = 205 ///| /// Partial Content: the byte ranges `Range` asked for. pub const HTTP_206_PARTIAL_CONTENT : Int = 206 ///| /// Multi-Status: WebDAV, one status per member of a collection. pub const HTTP_207_MULTI_STATUS : Int = 207 ///| /// Already Reported: WebDAV, this member was enumerated earlier in the reply. pub const HTTP_208_ALREADY_REPORTED : Int = 208 ///| /// IM Used: the body is the result of applying delta encodings. pub const HTTP_226_IM_USED : Int = 226 ///| /// Multiple Choices: several representations, pick one. pub const HTTP_300_MULTIPLE_CHOICES : Int = 300 ///| /// Moved Permanently: use `Location` from now on. pub const HTTP_301_MOVED_PERMANENTLY : Int = 301 ///| /// Found: a temporary move. Clients rewrite `POST` to `GET` here, which is why /// `307` exists. pub const HTTP_302_FOUND : Int = 302 ///| /// See Other: fetch the outcome with a `GET` at `Location` — the redirect after /// a form post. pub const HTTP_303_SEE_OTHER : Int = 303 ///| /// Not Modified: the client's cached copy is still current. pub const HTTP_304_NOT_MODIFIED : Int = 304 ///| /// Use Proxy: deprecated by RFC 9110. pub const HTTP_305_USE_PROXY : Int = 305 ///| /// Reserved: never standardised, and reserved so nothing else claims it. pub const HTTP_306_RESERVED : Int = 306 ///| /// Temporary Redirect: like `302`, but the method and body must be kept. pub const HTTP_307_TEMPORARY_REDIRECT : Int = 307 ///| /// Permanent Redirect: like `301`, but the method and body must be kept. pub const HTTP_308_PERMANENT_REDIRECT : Int = 308 ///| /// Bad Request: malformed enough that the server will not act on it. pub const HTTP_400_BAD_REQUEST : Int = 400 ///| /// Unauthorized: not authenticated. The reply must carry `WWW-Authenticate`. pub const HTTP_401_UNAUTHORIZED : Int = 401 ///| /// Payment Required: reserved. pub const HTTP_402_PAYMENT_REQUIRED : Int = 402 ///| /// Forbidden: authenticated, and still not allowed. pub const HTTP_403_FORBIDDEN : Int = 403 ///| /// Not Found. pub const HTTP_404_NOT_FOUND : Int = 404 ///| /// Method Not Allowed: the path exists under another verb, listed in `Allow`. pub const HTTP_405_METHOD_NOT_ALLOWED : Int = 405 ///| /// Not Acceptable: nothing on offer matches the request's `Accept`. pub const HTTP_406_NOT_ACCEPTABLE : Int = 406 ///| /// Proxy Authentication Required. pub const HTTP_407_PROXY_AUTHENTICATION_REQUIRED : Int = 407 ///| /// Request Timeout: the client took too long to send it. pub const HTTP_408_REQUEST_TIMEOUT : Int = 408 ///| /// Conflict: it clashes with the resource's current state. pub const HTTP_409_CONFLICT : Int = 409 ///| /// Gone: it existed and was removed on purpose. pub const HTTP_410_GONE : Int = 410 ///| /// Length Required: `Content-Length` is missing and this server insists on it. pub const HTTP_411_LENGTH_REQUIRED : Int = 411 ///| /// Precondition Failed: an `If-*` header did not hold. pub const HTTP_412_PRECONDITION_FAILED : Int = 412 ///| /// Content Too Large — Starlette's name keeps RFC 7231's "Request Entity Too Large". pub const HTTP_413_REQUEST_ENTITY_TOO_LARGE : Int = 413 ///| /// URI Too Long — Starlette's name keeps RFC 7231's "Request-URI Too Long". pub const HTTP_414_REQUEST_URI_TOO_LONG : Int = 414 ///| /// Unsupported Media Type: the body's `Content-Type` is one this route cannot read. pub const HTTP_415_UNSUPPORTED_MEDIA_TYPE : Int = 415 ///| /// Range Not Satisfiable: no part of the requested range exists. pub const HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE : Int = 416 ///| /// Expectation Failed: the `Expect` header cannot be met. pub const HTTP_417_EXPECTATION_FAILED : Int = 417 ///| /// I'm a Teapot: RFC 2324's joke, kept because clients test for it. pub const HTTP_418_IM_A_TEAPOT : Int = 418 ///| /// Misdirected Request: this connection is not authoritative for that authority. pub const HTTP_421_MISDIRECTED_REQUEST : Int = 421 ///| /// Unprocessable Content: well-formed, and it fails validation — what `unprocessable` /// returns. pub const HTTP_422_UNPROCESSABLE_ENTITY : Int = 422 ///| /// Locked: WebDAV, the resource is locked. pub const HTTP_423_LOCKED : Int = 423 ///| /// Failed Dependency: WebDAV, a request this one depended on failed. pub const HTTP_424_FAILED_DEPENDENCY : Int = 424 ///| /// Too Early: replaying this early-data request could be a replay attack. pub const HTTP_425_TOO_EARLY : Int = 425 ///| /// Upgrade Required: resend over the protocol named in `Upgrade`. pub const HTTP_426_UPGRADE_REQUIRED : Int = 426 ///| /// Precondition Required: the server refuses to act on an unconditional write. pub const HTTP_428_PRECONDITION_REQUIRED : Int = 428 ///| /// Too Many Requests: rate limited; `Retry-After` says when to come back. pub const HTTP_429_TOO_MANY_REQUESTS : Int = 429 ///| /// Request Header Fields Too Large. pub const HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE : Int = 431 ///| /// Unavailable For Legal Reasons. pub const HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS : Int = 451 ///| /// Internal Server Error: the fallback for an error the app did not map. pub const HTTP_500_INTERNAL_SERVER_ERROR : Int = 500 ///| /// Not Implemented: the server does not support the method at all. pub const HTTP_501_NOT_IMPLEMENTED : Int = 501 ///| /// Bad Gateway: an upstream sent something invalid. pub const HTTP_502_BAD_GATEWAY : Int = 502 ///| /// Service Unavailable: down or overloaded, and expected back. pub const HTTP_503_SERVICE_UNAVAILABLE : Int = 503 ///| /// Gateway Timeout: an upstream did not answer in time. pub const HTTP_504_GATEWAY_TIMEOUT : Int = 504 ///| /// HTTP Version Not Supported. pub const HTTP_505_HTTP_VERSION_NOT_SUPPORTED : Int = 505 ///| /// Variant Also Negotiates: the negotiation is configured in a circle. pub const HTTP_506_VARIANT_ALSO_NEGOTIATES : Int = 506 ///| /// Insufficient Storage: WebDAV, no room to store the representation. pub const HTTP_507_INSUFFICIENT_STORAGE : Int = 507 ///| /// Loop Detected: WebDAV, the traversal is cyclic. pub const HTTP_508_LOOP_DETECTED : Int = 508 ///| /// Not Extended. pub const HTTP_510_NOT_EXTENDED : Int = 510 ///| /// Network Authentication Required: a captive portal wants a login first. pub const HTTP_511_NETWORK_AUTHENTICATION_REQUIRED : Int = 511 // The WebSocket close codes of RFC 6455 §7.4.1, the second half of what // `status` re-exports. `1005`, `1006` and `1015` never travel on the wire — an // endpoint reports them to its own application, so sending one is a protocol // error rather than a close reason. ///| /// Normal Closure: the purpose the connection was opened for is fulfilled. pub const WS_1000_NORMAL_CLOSURE : Int = 1000 ///| /// Going Away: the peer is shutting down or navigating off the page. pub const WS_1001_GOING_AWAY : Int = 1001 ///| /// Protocol Error. pub const WS_1002_PROTOCOL_ERROR : Int = 1002 ///| /// Unsupported Data: a frame of a type this endpoint cannot accept. pub const WS_1003_UNSUPPORTED_DATA : Int = 1003 ///| /// No Status Received: reported locally when a close frame carried no code. pub const WS_1005_NO_STATUS_RCVD : Int = 1005 ///| /// Abnormal Closure: reported locally when the connection died without a close frame. pub const WS_1006_ABNORMAL_CLOSURE : Int = 1006 ///| /// Invalid Frame Payload Data: a text frame that is not valid UTF-8, say. pub const WS_1007_INVALID_FRAME_PAYLOAD_DATA : Int = 1007 ///| /// Policy Violation: the generic refusal when no other code fits. pub const WS_1008_POLICY_VIOLATION : Int = 1008 ///| /// Message Too Big. pub const WS_1009_MESSAGE_TOO_BIG : Int = 1009 ///| /// Mandatory Extension: the client required an extension the server did not negotiate. pub const WS_1010_MANDATORY_EXT : Int = 1010 ///| /// Internal Error: the server hit an unexpected condition. pub const WS_1011_INTERNAL_ERROR : Int = 1011 ///| /// Service Restart. pub const WS_1012_SERVICE_RESTART : Int = 1012 ///| /// Try Again Later: overloaded; come back. pub const WS_1013_TRY_AGAIN_LATER : Int = 1013 ///| /// Bad Gateway: the gateway got an invalid response upstream. pub const WS_1014_BAD_GATEWAY : Int = 1014 ///| /// TLS Handshake: reported locally when the handshake failed. pub const WS_1015_TLS_HANDSHAKE : Int = 1015

Continue: the headers are acceptable, send the body.

§Worked example

User structs that describe their own schema (derive(ToJson) + a T::schema() associated function + a ToSchema bridge) and a demo app whose request and response bodies surface fully-typed in openapi.json - the mctl-friendly shape.

struct
struct Address

A nested value type, to show a $ref chain: User.address references this under components/schemas.

fn
fn Address::schema() -> Schema

The Address descriptor.

item
impl ToSchema for Address with fn to_schema(_self)

Address's schema, so a route declaring it as a body or response documents itself.

struct
struct NewUser

The demo request model — the body of POST /users. age is optional.

fn
fn NewUser::schema() -> Schema

The NewUser descriptor.

item
impl ToSchema for NewUser with fn to_schema(_self)

NewUser's schema, so a route declaring it as a body or response documents itself.

struct
struct User

The demo response model — returned by both user routes. Nests Address and carries an array of tags, so its emitted schema exercises objects, $refs, and arrays together.

fn
fn User::schema() -> Schema

The User descriptor.

item
impl ToSchema for User with fn to_schema(_self)

User's schema, so a route declaring it as a body or response documents itself.

fn
fn demo_app() -> App

A demo application exercising the descriptor tree end to end: POST /users takes a typed NewUser body and returns a User; GET /users/:id takes a typed integer path param and returns a User. Both validate off their descriptor and both surface fully-typed bodies (with components/schemas $refs) in openapi.json.

item
extend Address with Eq::
item
extend Address with ToJson::
item
extend Address with ToSchema::
item
extend NewUser with Eq::
item
extend NewUser with ToJson::
item
extend NewUser with ToSchema::
item
extend User with Eq::
item
extend User with ToJson::
item
extend User with ToSchema::

§OAuth2 worked example

oauth2_app wires the security layer end to end: a /token endpoint that issues a scoped JWT and two protected routes, one requiring the items scope - FastAPI's security tutorial in explicit MoonBit form.

fn
fn oauth2_app(now : () -> Int64, secret? : String = "demo-secret") -> App

Build the OAuth2 demo application. secret is the shared HS256 key; now supplies the current Unix time (seconds) for both issuing and verifying, so a caller controls time in tests. Tokens live for one hour.

§Parameter constraints

The declared bounds a parameter carries — min/max, length, pattern, enum — checked on the way in and emitted into the schema on the way out, so the document and the enforcement cannot drift apart.

enum
enum Constraint

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

fn
fn with_constraints( j : Json, constraints : Array[Constraint], version : OpenApiVersion) -> Json

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.

fn
fn check_constraints( value : Json, constraints : Array[Constraint], loc : Array[String], errs : Array[ValidationError]) -> Unit

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.

item
extend Constraint with Eq::

§Security extractors

Pulling the credential out of a request for each scheme: the Authorization header, an API key in a header, query or cookie, and HTTP basic.

struct
struct HttpBasicCredentials

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

fn
fn parse_basic_auth(header : String) -> HttpBasicCredentials?

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.

fn
fn Context::http_basic(self : Context) -> HttpBasicCredentials?

The HTTP Basic credentials on this request (← FastAPI's HTTPBasic dependency), or None.

fn
fn Context::api_key_header(self : Context, name : String) -> String?

The API key carried in the request header name (← FastAPI's APIKeyHeader), or None. Header names are matched against the request's lower-cased headers.

fn
fn Context::api_key_query(self : Context, name : String) -> String?

The API key carried in the query parameter name (← FastAPI's APIKeyQuery), or None.

fn
fn Context::api_key_cookie(self : Context, name : String) -> String?

The API key carried in the cookie name (← FastAPI's APIKeyCookie), or None.

§Form & file extractors

Context::form reads both carriers a browser posts a form in - urlencoded and multipart - into one shape, bounded by the limits the caller gives, so a body over them is refused whole rather than truncated.

fn
fn Context::form( self : Context, limits? : @mime.Limits = @mime.limits) -> @mime.Form?

The posted form — FastAPI's Form(...) and File(...) parameters. Reading the body is moonhttp/mime's job; what belongs here is knowing that a request has a Content-Type and handing it over. None means the form broke its bounds and was refused whole: a handler given the first thousand parts of a larger form would be answering a request nobody sent. An empty form is the other answer, and means the request carried none.

item
using @mime

§Server-Sent Events

sse_response turns a stream of events into a text/event-stream response, one chunk per event so each dispatches on arrival, with the three headers that stop a proxy from buffering it into a file shaped like a stream.

fn
fn sse_response( events : Array[@sse.Event], status? : Int = 200, headers? : Array[(String, String)] = [], space? : Bool = true, wins? : @jwt.Wins = Extra, clash? : @jwt.OnClash[Array[(String, String)]] = Ignore) -> @moonasgi.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.

item
using @sse

§Access tokens

create_access_token names an algorithm and hands the signing to mooncred: the claims, the expiry and the extra members a caller adds, with the reserved ones it may not overwrite.

§Security guards

The check each scheme registers to run before the handler: the challenge a 401 owes, the 403 a missing scope earns, and the switch that lets an anonymous caller through for a route that would rather decide for itself.

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

fn
fn SecurityScopes::new(scopes? : Array[String] = []) -> SecurityScopes

The scopes required at one call site; empty means authentication only.

fn
fn SecurityScopes::list(self : SecurityScopes) -> Array[String]

The required scopes, in declaration order.

fn
fn SecurityScopes::scope_str(self : SecurityScopes) -> String

The scopes as OAuth2's single space-delimited string (← SecurityScopes.scope_str).

fn
fn SecurityScopes::challenge(self : SecurityScopes) -> String

The WWW-Authenticate value a bearer challenge carries: Bearer on its own, or Bearer scope="a b" when the route requires scopes, which is how RFC 6750 §3 tells a client what it was missing.

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

fn
fn OAuth2CodeBearer::new( authorization_url : String, token_url : String, secret : String, refresh_url? : String = "") -> OAuth2CodeBearer

Build an authorization-code bearer scheme. refresh_url is left out of the document when empty, since a provider that offers no refresh endpoint should not be described as having one.

fn
fn OAuth2CodeBearer::scheme( self : OAuth2CodeBearer, scopes? : Array[(String, String)] = []) -> SecurityScheme

The security scheme that describes this flow, advertising scopes.

enum
enum ApiKeyIn

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

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

fn
fn ApiKey::header(name : String, verify : (String) -> Bool) -> ApiKey

An API key read from the request header name (← APIKeyHeader).

fn
fn ApiKey::query(name : String, verify : (String) -> Bool) -> ApiKey

An API key read from the query parameter name (← APIKeyQuery).

fn
fn ApiKey::cookie(name : String, verify : (String) -> Bool) -> ApiKey

An API key read from the cookie name (← APIKeyCookie).

fn
fn ApiKey::read(self : ApiKey, ctx : Context) -> String?

The key this request presents for the scheme, None when it carries none.

fn
fn ApiKey::scheme(self : ApiKey) -> SecurityScheme

The apiKey scheme object this key is documented as.

struct
struct BasicAuth

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

fn
fn BasicAuth::new( verify : (HttpBasicCredentials) -> Bool, realm? : String = "") -> BasicAuth

Build an HTTP Basic scheme. A realm is optional and, when given, names the protection space in the challenge so a browser can tell one login from another.

fn
fn BasicAuth::challenge(self : BasicAuth) -> String

The WWW-Authenticate value this scheme challenges with.

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

fn
fn DigestAuth::new( verify : (String) -> Bool, realm? : String = "") -> DigestAuth

Build an HTTP Digest scheme, optionally naming the protection realm.

fn
fn DigestAuth::challenge(self : DigestAuth) -> String

The WWW-Authenticate value this scheme challenges with.

fn
fn Context::digest_credentials(self : Context) -> String?

The credential of an Authorization: Digest <parameters> header — the comma-separated parameter list, verbatim (← FastAPI's HTTPDigest). None when the header is absent or names another scheme; the scheme name is matched case-insensitively, as RFC 7235 requires.

fn
fn App::secure_api_key( self : App, name : String, key : ApiKey, description? : String = "", auto_error? : Bool = true) -> 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.

fn
fn App::secure_basic( self : App, name : String, basic : BasicAuth, description? : String = "", auto_error? : Bool = true) -> 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.

fn
fn App::secure_digest( self : App, name : String, digest : DigestAuth, description? : String = "", auto_error? : Bool = true) -> 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.

fn
fn App::secure_bearer( self : App, name : String, secret : String, bearer_format? : String = "JWT", description? : String = "", auto_error? : Bool = true) -> 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.

fn
fn App::secure_oauth2_code( self : App, name : String, code : OAuth2CodeBearer, scopes? : Array[(String, String)] = [], description? : String = "", auto_error? : Bool = true) -> 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.

fn
fn App::secure_openid( self : App, name : String, url : String, secret : String, description? : String = "", auto_error? : Bool = true) -> 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.

item
extend ApiKeyIn with Eq::

§Dependency injection worked example

A worked wiring of the container: providers, scopes and overrides, kept in the package so it is compiled and tested rather than only described.

struct
struct GreetReq

The request body of POST /greet. derive(@json.FromJson) lets Context::body_validated build it after the descriptor accepts the payload; its schema and struct fields agree (both require name), so a schema-valid body always deserialises.

fn
fn GreetReq::schema() -> Schema

The GreetReq descriptor — one required string field.

enum
enum Dep

The dependency value type of the greet app. A sum type wrapping every dependency this app injects — the explicit, exhaustive stand-in for FastAPI resolving heterogeneous Depends values dynamically.

fn
fn greet_app(container : Container[Dep]) -> App

Build the greet application over a caller-supplied dependency container, so a test can register dependency_overrides on the same container before or between requests. POST /greet resolves the "greeting" dependency, reads a validated GreetReq body, and answers {"message": "<greeting>, <name>"}; a malformed body gets a FastAPI-shaped 422. The dependency scope brackets each request, so any yield teardown runs once the handler returns.

item
extend GreetReq with Eq::
item
extend GreetReq with FromJson::
item
extend GreetReq with ToJson::
item
extend Dep with Eq::