moonapi builds a server application from typed routes and generates its own
OpenAPI document. One application object holds the routes, the layers around
them, the exception and per-status handlers, the security schemes, the lifespan
hooks and the OpenAPI metadata; the same object compiles to a server that runs
natively or in WebAssembly.
flowchart LR
routes["typed routes<br/>App::get / post / …"] --> app["**moonapi** App"]
app -->|"App::to_asgi()"| asgi(["moonasgi AsgiApp"])
app -->|"App::openapi_json()"| spec["OpenAPI 3.1"]
asgi --> cat["a server runs it"]
Routing and the OpenAPI document build on every backend. Handlers are async, and
serving needs a socket, so the entry point is async fn main and the target is
native or wasm:
#app.get("/", summary="Say hello")
fn hello(_ctx : @moonapi.Context) -> @moonasgi.Response {
@moonapi.text(200, "Hello from moonapi!")
}
#app.get("/users/{id}", summary="Fetch a user")
fn user(ctx : @moonapi.Context) -> @moonasgi.Response {
@moonapi.text(200, "user " + ctx.param("id").unwrap_or(""))
}
#app.post("/users", status_code="201")
fn create_user(_ctx : @moonapi.Context) -> @moonasgi.Response {
@moonapi.text(201, "created")
}
async fn main {
let app = @moonapi.App::new()
register(app)
// 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}")
}
}register comes from the generator, which reads the annotations at build time. Wire
it in once, in moon.mod:
rule(name: "routes", command: "moonx moonbitstack/moonapi/cmd/routes@0.17.9 -C \"$mod_dir\" \"$input\" -o \"$output\"")
and in the moon.pkg of the package holding the handlers:
dev_build(rule: "routes", input: "routes.mbt", output: "routes.g.mbt")
formatter(ignore: [ "routes.g.mbt" ])
#app.get, post, put, patch, delete, head, options and trace name their
verb, #app.route("PURGE", "/cache") takes any other, #app.websocket a socket, and
#router.* registers on a Router through register_router(router). Each takes the
same arguments as the method it stands for.
Annotations generate these calls, and routes built at run time are written the same way:
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_or(""),
), 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}")
}
}A type implementing View answers the methods of one path, and app.view registers each
method it answers. A method it leaves out answers 405, with Allow naming the ones it does:
struct User {}
impl @moonapi.View for User with get(_) {
Some(ctx => @moonapi.text(200, "user " + ctx.param("id").unwrap_or("")))
}
impl @moonapi.View for User with delete(_) {
Some(_ctx => @moonapi.text(204, ""))
}
async fn main {
let app = @moonapi.App::new()
app.view("/users/{id}", User::{})
@mooncat.serve(app.to_asgi(), port=12000) catch {
err => println("stopped: \{err}")
}
}app.urls takes a route table instead, kept apart from the handlers: path, view and
group each make an entry.
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 server and the app still runs in-process on native, wasm and
js: App::handle drives it through the same path a server does.
An app serves /openapi.json, /docs and /redoc from the start;
App::new(docs=None) leaves them off and enable_docs(...) changes them.
scalar_ui(), swagger_ui() and redoc_ui() return those pages on their own.
App::to_asgi() is the whole contract between an application and a server:
what runs it gets the same application — the layers, the mounts, the lifespan
hooks and the disconnect handling — and every way of serving below is one call
away from it. The server is a matter of the transport; the application does not
change when it moves to TLS or to another protocol version.
async fn main {
let app = @moonapi.App::new()
app.get("/", _ctx => @moonapi.text(200, "Hello"))
@mooncat.serve(app.to_asgi(), port=12000) catch {
err => println("stopped: \{err}")
}
}@mooncat.serve_tls serves the same application over TLS 1.3. The listener
offers h2 and http/1.1 by ALPN, so a client that negotiates h2 is served
HTTP/2 on the port that answers HTTP/1.1 to everyone else — wss:// included,
a WebSocket upgrade over TLS speaking whichever of the two it asked for. The
streams of one connection run concurrently, each in a task of its own, so a slow
handler delays its own reply and nobody else's. The certificate and the key are
PEM files read once at startup, the leaf first and any intermediates after it,
and a key that is not the certificate's is refused there rather than at the
first connection.
async fn main {
let app = @moonapi.App::new()
app.get("/", _ctx => @moonapi.text(200, "Hello over TLS"))
@mooncat.serve_tls(
app.to_asgi(),
certificate_file="certs/cert.pem",
private_key_file="certs/key.pem",
port=12000,
) catch {
err => println("stopped: \{err}")
}
}By default the same port also answers cleartext, so a client that reaches it
with http:// is served HTTP/1.1 or h2c and the redirect to https is the
application's to make — app.layer(@mid.HttpsRedirect::new()) is that layer.
cleartext=false makes the port TLS only, and the HTTP/2 limits come from
Config:
async fn main {
let app = @moonapi.App::new()
app.get("/", _ctx => @moonapi.text(200, "Hello over TLS"))
let config = @mooncat.Config::new(
port=8443,
cleartext=false,
h2=@mooncat.H2Config::new(max_streams=250),
)
@mooncat.serve_tls_config(
app.to_asgi(),
config,
{ certificate_file: "certs/cert.pem", private_key_file: "certs/key.pem" },
) catch {
err => println("stopped: \{err}")
}
}Config.h2 |
Default | Meaning |
|---|---|---|
max_streams |
100 | concurrent streams; the next one is refused, not queued |
max_header_list |
64 KiB | fields a request head may carry |
max_frame |
16 KiB | largest frame accepted |
window, connection_window |
1 MiB | how far a client may run ahead, per stream and per connection |
The limits a plain server applies hold per stream: past limit_concurrency a
stream is answered 503, every stream counts toward limit_max_requests, and a
connection with nothing open for timeout_keep_alive is ended with a GOAWAY.
A listener that speaks HTTP/2 and nothing else is serve_h2c, for a client that
starts with the connection preface because it was told the server speaks HTTP/2
— what a gRPC client does over a plaintext port:
@mooncat.serve_h2c(app.to_asgi(), port=12000)@quic.serve_config serves the same application over HTTP/1.1, HTTP/2 and HTTP/3
under one certificate, on one port number: TCP for the first two, UDP for the third,
with alt-svc on every response telling a client where HTTP/3 is. Each request
stream is one ASGI scope, so the application does not change. The QUIC half is native
only.
@quic.serve_config(
app.to_asgi(),
@config.Config::new(host="0.0.0.0", port=12000),
{ certificate_file: "cert.pem", private_key_file: "key.pem" },
)Behind a proxy that terminates QUIC itself, the app sees the forwarded scheme and host,
and the ASGI root_path under which it is mounted reaches /docs, /redoc and
/openapi.json alike.
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 src/examples/README.md. On Windows, macOS or Linux, start
the native server with cd integration && moon run src/full-example --target native, from a module of its own, since a server is not something this library depends on. The service
provides the API reference at http://127.0.0.1:12000/docs, ReDoc at
http://127.0.0.1:12000/redoc, and OpenAPI 3.0.3 at
http://127.0.0.1:12000/openapi.json.
The src/examples directory is a runnable tour of the public API. Each
numbered example has a runnable main package that isolates one feature:
| Example | Shows |
|---|---|
01-openapi |
building an OpenAPI document |
02-routing |
verbs, path parameters, and the 404-vs-405 distinction |
03-openapi-versions |
the same routes emitted as Swagger 2.0, OpenAPI 3.0 and 3.1 |
04-descriptors |
the descriptor tree behind a typed route |
05-constraints |
field constraints, documented and enforced |
06-typed-body |
validated and unchecked JSON body extraction |
07-request-extractors |
query, headers and body from the context |
08-response-model |
filtering a handler's return to a declared model |
09-dependency-injection |
the container, scopes and teardown |
10-oauth2-jwt |
an OAuth2 password grant and bearer enforcement |
11-per-op-security |
security requirements on individual routes |
12-security-schemes |
the six OpenAPI scheme shapes |
13-forms-files |
form and file uploads with size bounds |
14-background-tasks |
work deferred until after the response |
15-mounting |
composing sub-applications under a prefix |
16-middleware |
layers and middleware around the router |
17-sse |
server-sent events |
18-websocket |
a live WebSocket connection |
19-json-schema |
JSON schema generation |
20-views |
one type answering the methods of a path |
21-route-table |
a route table kept apart from its handlers |
22-docs-ui |
the documentation pages |
23-lifespan |
startup and shutdown hooks |
24-files |
serving files with range and conditional handling |
26-described-model |
describing a model once, using it four ways |
27-annotations |
registering routes from annotated functions |
28-templates |
rendering HTML templates |
integration |
one description driving validation, the document, the table and the rows |
Run any of them with, for example:
moon run src/examples/02-routing --target native
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. templates and static are the exception: they bring
dependencies of their own, so a program imports them by name.
| Package | What it holds | Depends on |
|---|---|---|
moonapi |
App and its methods, the façade |
everything below |
moonapi/templates |
Templates: pages rendered into responses, with request and url_for; load_directory (native) |
moonpug, ctx |
moonapi/static |
serve, a Source trait, Memory and Directory (native) |
ctx |
moonapi/route |
Route, WsRoute, Router, path templates; Method from moonhttp |
ctx, endpoint, security, socket |
moonapi/cmd/routes |
the annotation generator, run as a build rule | moonast |
moonapi/openapi |
the document's info block, routes as operations | route, schema |
moonapi/security |
schemes, guards, per-operation requirements, tokens | ctx, schema, status |
moonapi/exceptions |
HttpException and exception handlers |
ctx |
moonapi/endpoint |
Param, ResponseSpec, Endpoint and its validation |
ctx, schema |
moonapi/ctx |
Context and every extractor, background tasks |
schema, status |
moonapi/schema |
Schema, constraints, ValidationError, of_model |
status |
moonapi/socket |
the live WebSocket connection | nothing |
moonapi/status |
response helpers, streams, SSE, cookies, status constants | nothing |
moonapi/di |
dependency injection | nothing |
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 @exceptions.HttpException(...).
- Routing —
App::get/post/put/patch/delete/routeon path templates:{name}binds one segment or part of one,{id:int},{x:float}and{id:uuid}bind only what fits,{rest:path}binds the rest slashes included, and the result lands inContext::param. The convertor also types the parameter in the OpenAPI document. Correct404(no path) versus405(path but not method), and a path off by a trailing slash answers307to the one that exists. A route takes the operation arguments —summary,description,tags,deprecated,operation_id,status_code,responses,name,include_in_schema, and anopenapi_extrafragment merged over the generated operation object — andApp::url_for(name, params)resolves a named route back to its path, mount prefix included. - Regex and glob routes —
regex=andglob=on any route decide which paths it answers, in the annotation and in the method alike:#app.get("/users/{id}", regex="/users/(?P<id>[0-9]+)").pathstays what the OpenAPI document andurl_forshow. Either has to match the whole path; a regular expression's named groups are the parameters, and a glob's*and**bind the template's parameters in order. Routes are tried in the order they were registered, and the first that matches answers. Giving both aborts at registration. - Routers —
Routercollects routes away from any application, andApp::include_router(router, prefix=, tags=, security=, dependencies=, responses=, deprecated=, include_in_schema=)folds them in as the app's own. 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, anddeprecated/include_in_schemamark or hide the whole set. A router is a value, so the same one can be included twice under different prefixes. Routers include routers, so a large API is a tree included once. - Middleware — middleware on a route or a router node (
hoops=,Router::hoop): it runs inside the app's middleware and outside the route's security, validation and dependencies, so it can answer for the route, change its reply, or hand the rest a context of its own. The hoops of include calls and routers wrap the route's own, outermost first. - Views and route tables — a type implementing
Viewanswers the methods of one path, and a request for any other gets a405whoseAllowlists what it does answer.app.urls([path(...), view(...), websocket(...), group("/blog", [...])])registers a route table kept apart from the handlers, with groups nesting and carrying tags, security, dependencies and hoops. - Descriptor tree — a runtime
Schema/Param/Endpointtree 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,nullablein 3.0,x-nullablein 2.0), aMap[String, T]asadditionalProperties,Anyas the empty schema, aformatsuch asbinary, and defaults, with named models hoisted undercomponents/schemasand referenced by$ref— and (b) drive request validation off the same tree. AParamcarries the same constraints aFielddoes, plus adefaultand analias, so a query parameter's bounds are both documented and enforced, and a422points at the name the client actually sent. User structs describe themselves withderive(ToJson)+ aT::schema()associated function + a one-lineToSchemabridge. - Multi-version OpenAPI —
App::openapi_jsonemits Swagger 2.0, OpenAPI 3.0.3, and OpenAPI 3.1.0 from the same routes and descriptors (3.xrequestBody+components/schemas; 2.0 body-parameter +definitions).App::describeadds the summary, tag descriptions and external documentation;app.webhooksdocuments the requests the API sends out (3.1webhooks); a route'scallbacksdocuments the requests an operation makes back to its caller; aParam, a request body, aResponseSpecand aFieldeach take namedexamples; and a route'sresponse_classstates what its success body is —Text,Html,Xml,EventStream,Bytes— so the document carries that media type (asproducesin Swagger 2.0) instead of assuming JSON. - 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 located422error list — schema emission, validation, and deserialisation all off one descriptor. - Dependency injection — a
Container(provider registry) with request-scoped, one-shot resolution (per-request caching) andyield-style teardown run LIFO around the handler.App::depends(container.erase())attaches one to the application, and any route builder takesdependencies=["key"]: 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
/tokenpassword-grant endpoint issues an HS256 JWT (create_access_token), and anOAuth2PasswordBearerreads theAuthorization: Bearerheader, verifies the token, and enforces scopes:401on a missing or invalid/expired token,403when a valid token lacks a required scope. The token itself ismooncred's and the algorithms aremooncrypt'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. - Form & file extractors —
Context::formparses both anapplication/x-www-form-urlencodedbody (percent- and+-decoded) and amultipart/form-databody, splitting the boundary stream into fields and byte-exact uploads (filename + content-type +size+ the part's own headers + raw bytes). Reading the body ismoonhttp/form's, over moonmime and moonurl; a body is attacker-controlled and already buffered, so@moonmime.Limitsbounds what one may spend — a thousand parts of a megabyte by default — and a body over either bound comes back asNone, refused whole rather than truncated.Context::oauth2_password_formreads the OAuth2 password form off it. - response_model —
filter_response/json_modelvalidate a handler's return value against a declaredSchemaand 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 — two kinds of middleware.
App::layertakes any moonasgi@mid.Layer, which wraps the ASGI application and sees every event, so streams stay streams and WebSocket traffic passes through: moonasgi shipsCors,Gzip,TrustedHosts,HttpsRedirectandServerErrors, andWrapturns any function from app to app into one.App::middlewareis a function of the request andcall_nextthat returns the response, whole. Layers and middleware wrap the app in one list, in registration order with the first outermost, and they wrap mounted applications too. Exception handlers (App::exception_handler): a handlerraises anHttpExceptionand the app maps it to a response, falling through to a built-in{"detail": ...}forHttpExceptionand a500for anything else.App::add_status_handler(code, ...)swaps in a custom response for any error status — a routing404/405or 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 —401with theWWW-Authenticatechallenge the scheme owes (a scoped bearer route names the scope it wanted),403on a missing scope or a rejected API key. Every one takesdescription=andauto_error=(with the flag off, the guard admits an anonymous caller and leaves the decision to the route). A bareadd_security_schemedocuments without enforcing. - Background tasks — every handler has
ctx.background;add_taskdefers async work that the app runs, in order, after the last body event is sent, so a slow write never delays the client. - Sub-application mounting —
App::mount(prefix, subapp)composes applications: a request underprefixreaches the sub-app with the prefix moved frompathtoroot_path(its own layers, 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, and a sub-app's lifespan hooks run with the parent's.App::mount_asgi(prefix, app)mounts any other ASGI application 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 aStream, whose body writes through aWriter: eachwriteis one body event, so a client reads the first long before the last one exists, and HTTP trailers follow the body when the route declares them. A client that leaves cancels the body. Layers see each event as it goes; anApp::middlewaregets the response whole, so the stream is gathered for it. - Server-Sent Events —
moonhttp/sseframes an event per the WHATWG event-stream format (id/event/retry/ multi-linedata/:comments);sse(body)is atext/event-streamStreamwhose body sends those frames throughEvents::sendandping, one body event per frame, so each dispatches on arrival. A client that reconnects sends the last id it saw, andctx.last_event_id()lets the stream pick up after it. - WebSocket routes —
App::websocket(path, handler)over the moonasgi WebSocket protocol. The async handler holds the live connection:accept(with a subprotocol and headers),receive/receive_text/receive_bytes/receive_json, the matchingsend_*, andclose. It can speak first and answer as messages arrive; a client that leaves raisesDisconnected(code~, reason~)out ofreceive, which ends the handler quietly unless it catches it. A route takessecurity,dependenciesand anamelike an HTTP route; a refused connection is declined at the handshake with the HTTP answer where the server allows it, or close code 1008. The moonasgi test client'swebsocketdrives a route in-process. - 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/securitySchemesin 3.x,securityDefinitionsin 2.0, where anhttpscheme becomes the standardapiKey-in-Authorizationworkaround and OpenID Connect — which 2.0 cannot express — is left out rather than described as something it is not. - Documentation UI —
/openapi.json,/docsand/redocare on by default (App::new(docs=None)to leave them off,enable_docs(...)or aDocsrecord to change them), all three withinclude_in_schema=falseso turning documentation on does not change the document./docsis the Scalar reference by default and Swagger UI withui=Swagger(with its OAuth2 redirect page andinit_oauth); every reader is loaded at a pinned version, and behind a proxy the pages and the document's servers follow the ASGIroot_path.scalar_ui(),swagger_ui()andredoc_ui()return the pages alone, for an app that serves them under its own paths or behind its own guard. - Templates —
moonapi/templatesrenders moonpug templates into responses: every page seesrequest,url_forresolves named routes, context processors add names to every page, and where the call'scontextand a processor name the same thing the rule is published (wins,clash).load_directoryreads a template tree once on native. - Static files —
moonapi/staticserves aSourceunder a mount with the same conditional and range handling asContext::serve, a405for anything butGETandHEAD, no way out of the source, and anhtmlmode with indexes, the trailing-slash redirect and a404.html.Memoryworks on every target,Directoryreads the disk on native, and a source of your own is one trait implementation. - Lifespan hooks —
App::on_startup/App::on_shutdown, driven over the ASGI lifespan protocol. Startup in registration order, shutdown in reverse, so a resource is released before whatever opened it. A hook gets the lifespan state, and what it stores there reaches every request asctx.state(). The moonasgi test client'slifespanruns the same thing in-process. - Responses —
text,html,jsonandjson_modelhelpers overmoonasgi.Response, plusredirect(url, status=307)(the URL is encoded over the characters a URI reserves for structure, so an already-encoded URL passes through and a smuggledCRLFcannot open a header) andfile_response(content, filename=, media_type=, inline=)(a media type guessed from the extension,Content-Length, and aContent-Dispositionthat falls back to RFC 6266'sfilename*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, ...)isfile_responsewith the request weighed:304when the client's copy is current,412when a precondition fails,206for a range (oneContent-Range, ormultipart/byterangesfor several),416for 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'sconditionalandrange, so it is RFC 9110 §13 and §14 rather than a guess. Everything turns onetag=; 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 adata:URL carries, under the media type it names;data_url(content, media_type=)writes one. This is whatcanvas.toDataURL()andFileReader.readAsDataURL()hand back, and how a client posts an image inside a JSON field. Not a data URL yieldsNone, 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=)anddelete_cookie(...). Each returns a new response carrying one moreSet-Cookie, which is the correct wire shape: two cookies are two headers, never one folded field.expiresis a@moondate.Moment, because an expiry is a moment and the format says how to write one;delete_cookieexpires by bothMax-Age=0and the epoch.Context::cookiereads 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'scookiepackage, which a client uses from the other side. - Status constants — the
HTTP_*andWS_*names, so a route table shows a deliberate307rather than a bare number. - One description, several uses —
Schema::of_model(model, known=)takes a moonmodel description and gives back a schema, so one model declaration drives both validation and the OpenAPI document. ARefis looked up inknown; 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-writtenSchematree is still there and still the shorter way to say something small.
Nothing here decides for you twice. Every bound is an argument with a published default, and every default is a sensible one.
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(body, headers=[("cache-control", "no-store")])
@moonapi.sse(body, space=false)| Setting | Default | Why that one |
|---|---|---|
query_limits.pairs |
1000 | a thousand query parameters is a generous ceiling |
query_limits.size |
64 KiB | past the 8 KiB the common servers allow a whole request line |
form's limits |
@moonmime.limits |
a sane file and field count, with a part size bound |
expires_in_secs |
3600 | the hour a token grant issues |
space on a stream |
on | the space after a colon is universal on the wire and stripped by every reader |
redirect_slashes |
on | a link written with or without its trailing slash still arrives |
create_access_token computes sub, iat, exp and scopes from its
arguments, and extra adds claims alongside them. sse 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:
| Function | Who wins by default | What happens when both speak |
|---|---|---|
create_access_token |
the arguments | aborts — a token whose subject is not the subject passed in is a mistake in the program |
sse |
the caller's header | replaces 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.
- One path for every caller.
App::to_asgiis the application;App::handle, the test client and a server all go through it, so a test exercises the layers, mounts, lifespan state and disconnect handling a server does, not a shortcut beside them. - Middleware that is not moonapi's lives in moonasgi. CORS, gzip and the host checks are ASGI layers, so a moonapi app and any other ASGI application share one implementation and one configuration.
Next: writing a request's descriptor from the struct it deserialises into, rather than by hand, and splitting the router from the OpenAPI writer so a program that builds an application and never serves a document links only what it uses.
The site at moonbitstack.github.io/moonapi has a guide, the full API reference generated from the package interfaces and source comments, an examples gallery and a request lifecycle showcase.
Apache-2.0.