Lightweight HTTP web framework and router for MoonBit, inspired by Chi
moon add jaredzhou/pony///|
fn main {
let r = Router::Router()
// GET /ping => 200 "pong"
r.add(HttpMethod::Get, "/ping", fn(ctx) { ctx.write_text(status_ok, "pong") })
// GET /users/{id} => 200 "user: 42"
// ctx.param raises PonyError if missing — use try_param for Option
r.add(HttpMethod::Get, "/users/{id}", fn(ctx) {
let id = ctx.try_param("id").unwrap_or("")
ctx.write_text(status_ok, "user: \(id)")
})
// GET /search?q=pony => 200 "searching: pony"
r.add(HttpMethod::Get, "/search", fn(ctx) {
let q = ctx.try_query("q").unwrap_or("")
ctx.write_text(status_ok, "searching: \(q)")
})
// GET /api/status => 200 {"status":"ok"}
r.add(HttpMethod::Get, "/api/status", fn(ctx) {
ctx.reply_ok({ "status": "ok" })
})
// POST /api/items — standard error pattern with to_api_error
r.add(HttpMethod::Post, "/api/items", async fn(ctx) {
let req : ItemReq = ctx.json() catch {
e => {
ctx.reply_error(to_api_error(PonyError::InvalidArgument(e.to_string())))
return
}
}
ctx.reply_ok({ "created": true })
})
// GET /static/css/app.css => 200 "file: css/app.css"
r.add(HttpMethod::Get, "/static/*", fn(ctx) {
let path = ctx.try_wildcard().unwrap_or("")
ctx.write_text(status_ok, "file: \(path)")
})
start("127.0.0.1:3000", r)
}r.use_mw(@mw.logger())
r.use_mw(@mw.cors(
allow_origins=["*"],
allow_methods=["GET", "POST"],
))
r.use_mw(@mw.jwt(new_hmac_sha256(secret)))| Middleware | Description |
|---|---|
| logger() | Request logging (method, path, status, duration) |
| cors() | CORS headers with configurable origins, methods, headers, max-age |
| jwt(signing_method) | JWT bearer token auth, stores sub claim in context |
///|
// Marker type — an empty struct used as a type-safe key
struct UserId {}
///|
// Middleware: extract user_id from header and store in context
fn auth_middleware(next : Handler) -> Handler {
fn(ctx) {
match ctx.try_header("X-User-Id") {
Some(user_id) => ctx.set_ext(UserId{}, user_id)
None => {
ctx.reply_error(ApiError::new(unauthenticated, "missing X-User-Id header"))
return
}
}
next(ctx)
}
}
///|
fn main {
let r = Router::Router()
r.use_mw(auth_middleware)
// Handler reads user_id from ext store — raises PonyError::MissingExt if not set
r.add(HttpMethod::Get, "/me", fn(ctx) {
let user_id : String = ctx.get_ext(UserId{}) catch {
e => {
ctx.reply_error(to_api_error(e))
return
}
}
ctx.reply_ok({ "user_id": user_id })
})
start("127.0.0.1:3000", r)
}| Raising version (new) | Option version (try_*) | Description |
|---|---|---|
| ctx.param("id") | ctx.try_param("id") | Path parameter (raises PonyError::MissingParam) |
| ctx.query("search") | ctx.try_query("search") | Query string value (raises PonyError::MissingQuery) |
| ctx.header("Accept") | ctx.try_header("Accept") | Request header (raises PonyError::MissingHeader) |
| ctx.form("name") | ctx.try_form("name") | Form field — async (raises PonyError::MissingForm) |
| ctx.wildcard() | ctx.try_wildcard() | Wildcard path capture (raises PonyError::MissingParam) |
| ctx.json[T]() | (catch pattern) | JSON body deserialization |
| ctx.form_file("file") | ctx.try_form_file("file") | Uploaded file header (raises PonyError::MissingFormFile) |
///|
let user = ctx.header("X-User") catch {
e => {
ctx.reply_error(to_api_error(e))
return
}
}///|
let q = ctx.try_query("search").unwrap_or("")
///|
let page = ctx.try_query("page").unwrap_or("1")ctx.set_content_type("application/json")
ctx.write_text(200, "Hello")
ctx.write_json(200, {"key": "value"})
ctx.reply_ok({"status": "ok"}) // 200 JSON
ctx.reply_error(to_api_error(PonyError::InvalidArgument("bad input"))) // error JSON
ctx.reply_error(ApiError::new(invalid_argument, "bad input")) // direct ApiError
ctx.redirect("/login") // 302 redirect
ctx.no_content() // 204///|
pub suberror PonyError {
MissingParam(String)
MissingQuery(String)
MissingForm(String)
MissingHeader(String)
MissingExt(String)
ExtDecodeError(String, String)
MissingFormFile(String)
InvalidArgument(String) // generic 400
NotFound(String) // generic 404
PermissionDenied(String) // generic 403
}ctx.reply_error(to_api_error(PonyError::InvalidArgument("invalid id")))
ctx.reply_error(to_api_error(PonyError::NotFound("list not found")))///|
let req : MyBody = ctx.json() catch {
e => {
ctx.reply_error(to_api_error(PonyError::InvalidArgument(e.to_string())))
return
}
}ctx.set_ext(@pony.RequestId{}, "req-abc")
ctx.set_ext(@pony.UserId{}, "user-42")
let uid = ctx.get_ext(@pony.UserId{})let h = @pony.new()
h.set("Content-Type", "application/json")
h.add("Set-Cookie", "session=abc")
let ct = h.get("Content-Type") // "application/json"let server = @pony.Server::new("127.0.0.1:3000", router)
.with_timeout(read=5000, write=10000)
server.start()!pub trait Closer {
fn close(Self) -> Unit
}pub(all) suberror MultipartError {
MissingBoundary
MissingName
MalformedBody(String)
FileTooLarge(String, Int64)
FileIOError(String)
} derive(Debug)pub(all) struct Context {
http_method : HttpMethod
url_str : String
url : URL
route_path : String
req_headers : HttpHeaders
resp_headers : HttpHeaders
queries : Values
form_values : Values?
multipart_form : MultipartForm?
exts : ExtStore
path_params : Array[ParamKV]
req_body : &Reader
resp_writer : ServerConnection
}async fn Context::parse_multipart_form(self : Context, max_memory? : Int64, writers? : Map[String, FileWriter]) -> Unitpub(all) struct FileHeader {
filename : String
size : Int64
content_type : String?
header : PartHeaders
backend : FileBackend
}type MultipartFormpub(all) struct Part {
source : &Reader
delimiter : Bytes
headers : PartHeaders
buf : Buffer
body_eof : Bool
}pub(all) struct RequestId {
}fn Router::add(self : Router, meth : HttpMethod, pattern : String, name? : String, mws? : Array[Middleware], handler : Handler) -> Unit raise RouterErrorfn Router::any(self : Router, pattern : String, name? : String, mws? : Array[Middleware], handler : Handler) -> Unit raise RouterErrorasync fn Router::handle(self : Router, req : Request, req_body : &Reader, resp_writer : ServerConnection) -> Unitpub struct Server {
router : Router
addr : String
read_timeout : Int?
write_timeout : Int?
request_id : Int
}pub(all) struct UserId {
}let default_max_memory : Int64let status_service_unavailable : Intlet status_unavailable_for_legal_reasons : Intlet unavailable : IntInstall
Download zipLightweight HTTP web framework and router for MoonBit, inspired by Chi