A Gin-inspired web framework for MoonBit — fast, lightweight, idiomatic.
mbit is a fast, lightweight web framework for MoonBit — batteries-included for production-ready Web development.
async fn main {
let app = @mbit.default()
app.get("/", [fn(ctx) { ctx.string(200, "Hello, mbit!") }])
app.get("/hello/:name", [fn(ctx) {
let name = ctx.param_default("name", "world")
ctx.string(200, "Hello, " + name + "!")
}])
app.run("0.0.0.0:8080")
}import {
"RabitLogic/mbit",
}app.get("/users", [handler])
app.post("/users", [handler])
app.put("/users/:id", [handler])
app.delete("/users/:id", [handler])
app.patch("/users/:id", [handler])
app.head("/health", [handler])
app.options("/api", [handler])
// Match all HTTP methods
app.any("/health", [handler])
// Register with explicit methods
app.handle([GET, POST], "/multi", [handler])app.get("/users/:id", [fn(ctx) {
let id = ctx.param("id") // Some("42")
let id_int = ctx.param_int("id") // Some(42)
let id_str = ctx.param_default("id", "0")
let all = ctx.params_all() // Map of all params
}])app.get("/files/*filepath", [fn(ctx) {
let path = ctx.param_default("filepath", "")
ctx.string(200, "Requested: " + path)
}])let api = app.group("/api")
api.get("/status", [status_handler])
api.post("/login", [login_handler])
// Nested groups
let admin = api.group("/admin")
admin.use(auth_middleware)
admin.get("/dashboard", [dashboard_handler])app.no_route([fn(ctx) {
ctx.json(404, Json::object({"error": Json::string("Not Found")}))
}])
app.no_method([fn(ctx) {
ctx.json(405, Json::object({"error": Json::string("Method Not Allowed")}))
}])app.redirect_trailing_slash(true) // /foo/ → /foo
app.redirect_fixed_path(true) // case-insensitive fix
app.remove_extra_slash(true) // //foo → /foo
app.handle_method_not_allowed(false) // disable 405| Middleware | Description |
|---|---|
| logger() | Logs method, path, status, latency |
| structured_logger(config) | Structured JSON/text logging with log levels + fields |
| recovery() | Catches panics, returns 500 |
| cors(config) | CORS headers with dynamic origin validation |
| gzip(config) | Response compression |
| request_id() | X-Request-Id header |
| timing() | X-Response-Time header |
| secure(config) | Security headers (helmet-like) + CSRF protection |
| auth() | Basic auth |
| rate_limit(config) | Rate limiting (token bucket / sliding window / fixed window) |
| body_size_limit(bytes) | Reject large payloads |
| metrics_middleware(collector) | Request metrics collection (counts, latency, status codes) |
// Global middleware (applies to all routes)
app.use(logger())
app.use(recovery())
app.use(cors(CORSConfig::default()))
// Group middleware (applies to routes in that group)
let api = app.group("/api")
api.use(auth())
api.use(rate_limit(RateLimitConfig::{ max_requests: 100, window_secs: 60 }))app.use(logger())
// Output: [mbit] GET /api/users -> 200 (12ms)// Static origins
app.use(cors(CORSConfig::{
allow_origins: ["https://example.com"],
allow_methods: ["GET", "POST"],
allow_headers: ["Content-Type", "Authorization"],
allow_credentials: true,
max_age: 3600,
expose_headers: ["Content-Length"],
}))
// Dynamic origin validation
app.use(cors(CORSConfig::with_origin_fn(fn(origin) {
origin.has_suffix(".trusted.com") || origin == "https://app.example.com"
})))app.use(gzip(GzipConfig::{ min_length: 512, level: 6 }))// Fixed window (default)
app.use(rate_limit(RateLimitConfig::{
max_requests: 50,
window_secs: 60,
}))
// Token bucket (smooth rate limiting)
app.use(rate_limit(RateLimitConfig::token_bucket(
RateTokenBucket::{ rate: 10.0, burst: 20 },
)))
// Per-route rate limiting
app.use(rate_limit(
RateLimitConfig::default().when(fn(ctx) { ctx.path().has_prefix("/api/") })
))
// Per-user rate limiting
app.use(rate_limit(
RateLimitConfig::default().with_key(fn(ctx) { ctx.param_default("api_key", "unknown") })
))// Security headers only
app.use(secure(SecureConfig::default()))
// With CSRF protection
app.use(secure(SecureConfig::default().with_csrf()))
// Sets: X-Frame-Options, X-Content-Type-Options, X-XSS-Protection,
// Content-Security-Policy, Referrer-Policy, HSTS, CSRF tokenapp.get("/ws", [websocket_handler(fn(ws) {
ws.send_text("Connected!")
loop {
match ws.read() {
Some(WebSocketMessage::Text(t)) => ws.send_text("Echo: " + t)
Some(WebSocketMessage::Close(_, _)) => break
_ => ()
}
}
})])
// Manual upgrade
app.get("/ws2", [fn(ctx) {
let ws = upgrade_websocket(ctx)
match ws {
Some(ws) => ws.send_text("Hello via WebSocket!")
None => ctx.abort_with_status(400, "WebSocket upgrade failed")
}
}])// Request logging with structured fields
app.use(structured_logger(StructuredLogConfig::default()))
// Log with levels and fields anywhere
Logger::info("User logged in", fields=[("user_id", "42"), ("ip", ctx.client_ip())])
Logger::error("Database timeout", fields=[("query", "SELECT ...")])
Logger::debug("Cache miss", fields=[("key", "user:42")])
// Output (text format):
// [2026-07-22T10:00:00] [INFO] User logged in app=mbit env=development user_id=42 ip=10.0.0.1
// Configure
Logger::set_level(Info)
Logger::set_format(JSONFormat)
// Metrics collection
let metrics = MetricsCollector::new()
app.use(metrics_middleware(metrics))
app.get("/metrics", [fn(ctx) { ctx.json(200, metrics.to_json()) }])fn auth_middleware() -> Handler {
async fn(ctx) {
match ctx.header("Authorization") {
Some(token) => {
ctx.set("token", Json::string(token)) // store for downstream handlers
ctx.next() // continue the chain
}
None => ctx.abort_with_status(401, "Unauthorized")
}
}
}
app.use(auth_middleware())ctx.http_method() // "GET", "POST", etc.
ctx.path() // "/users/42"
ctx.full_path() // "/users/42?page=1"
ctx.client_ip() // client IP address
ctx.content_type() // "application/json"
ctx.header("Accept") // optional header value
ctx.is_websocket() // true if upgrade requestctx.query("page") // Some("1")
ctx.query_default("page", "1") // "1"
ctx.query_int("page") // Some(1)
ctx.query_int64("id") // Some(42L)
ctx.query_map() // all query params as Map
ctx.query_array("tags") // ["a", "b"] for ?tags=a&tags=bctx.body_string() // raw body as string
ctx.body_json() // parsed as Json
// Form data (application/x-www-form-urlencoded)
ctx.post_form("name") // Some("Alice")
ctx.default_post_form("name", "unknown")
ctx.post_form_map() // all form fields// JSON
ctx.json(200, Json::object({"message": Json::string("ok")}))
ctx.json_indented(200, data)
ctx.json_secure(200, data) // with while(1) prefix
ctx.json_pure(200, data) // no escaping
// XML & YAML
ctx.xml(200, "root", data)
ctx.yaml(200, data)
// HTML & Text
ctx.html(200, "<h1>Hello</h1>")
ctx.string(200, "plain text")
// Redirect
ctx.redirect(301, "/new-path")
// File
ctx.file("path/to/file.pdf")
// SSE (Server-Sent Events)
ctx.sse("event: message\ndata: hello\n\n")
// Streaming
ctx.stream(200, fn(writer) {
writer("chunk1")
writer("chunk2")
})ctx.set("user_id", Json::string("42"))
let user_id = ctx.get("user_id") // Some(Json::String("42"))
ctx.get_string("user_id") // Some("42")ctx.set_header("X-Custom", "value")
ctx.status_code // current status (default 200)
ctx.is_written() // has response been sentstruct LoginRequest {
username : String
password : String
} derive(@json.FromJson)
// JSON body
app.post("/login", [async fn(ctx) {
let req : LoginRequest? = bind_json(ctx)
match req {
Some(r) => ctx.json(200, Json::object({"user": Json::string(r.username)}))
None => ctx.abort_with_status(400, "Invalid JSON")
}
}])
// Query string: GET /search?q=hello&page=1
struct SearchQuery {
q : String
page : String
} derive(@json.FromJson)
let query = bind_query(ctx)
// Form body
let form = bind_form(ctx)
// Headers
let headers = bind_header(ctx)
// Path params
let params = bind_path(ctx)let req = ctx.must_bind_json::[LoginRequest]() // aborts with 400 on failurelet rules = [
FieldRules::new("username", [Required, Min(3), Max(20)]),
FieldRules::new("email", [Required, Email]),
FieldRules::new("age", [Min(0), Max(150)]),
]
let errors = validate_map(values, rules)
if errors.length() > 0 {
ctx.abort_with_status(422, "Validation failed")
return
}| Rule | Description |
|---|---|
| Required | Must not be empty |
| Min(n) | Minimum length or numeric value |
| Max(n) | Maximum length or numeric value |
| Len(n) | Exact length |
| Pattern(regex) | Must match pattern |
| OneOf([...]) | Must be one of the values |
| Basic email check | |
| URL | Basic URL check |
| Custom(fn) | Custom validation function |
app.set_html_template("welcome", "<html><body>Hello {{.Name}}!</body></html>")
app.set_template_delims("{{", "}}")
// In handler
ctx.html_template("welcome", {"Name": "World"})// Single file
app.static_file("/favicon.ico", "./public/favicon.ico")
// Directory
app.static_("/static", "./public")
// Directory with wildcard
app.static_fs("/assets", "./public")// Basic
app.run("0.0.0.0:8080")
// Default (logs port)
app.run_default()
// Built-in health check endpoint
app.health() // registers GET /health → {"status":"ok","service":"mbit"}
// Graceful shutdown — call shutdown() from a handler or signal
app.run_with_shutdown(":8080")
// Elsewhere: app.shutdown() closes the server
// Trigger shutdown from signal handler
app.shutdown(cleanup=fn() {
println("Cleaning up resources...")
})let routes = app.routes()
for r in routes {
println(r.meth + " " + r.path)
}
println(app.trees()) // ASCII tree of the routerlet engine = Engine::new()
engine.max_multipart_memory(32 * 1024 * 1024) // 32 MB
engine.redirect_trailing_slash(true)
engine.handle_method_not_allowed(true)
engine.remove_extra_slash(true)
engine.redirect_fixed_path(false)
let app = Mbit::new(engine)async fn main {
let app = @mbit.default()
// Global middleware
app.use(logger())
app.use(recovery())
app.use(cors(CORSConfig::default()))
app.use(request_id())
app.use(timing())
// Custom 404
app.no_route([fn(ctx) {
ctx.json(404, Json::object({"error": Json::string("Not Found")}))
}])
// Routes
app.get("/", [fn(ctx) { ctx.string(200, "Hello, mbit!") }])
app.get("/hello/:name", [fn(ctx) {
ctx.string(200, "Hello, " + ctx.param_default("name", "world") + "!")
}])
app.post("/api/echo", [async fn(ctx) {
match ctx.body_json() {
Some(json) => ctx.json(200, json)
None => ctx.abort_with_status(400, "Invalid JSON")
}
}])
// Protected group
let api = app.group("/api")
api.use(fn(ctx) {
match ctx.header("Authorization") {
Some(_) => ctx.next()
None => ctx.abort_with_status(401, "Unauthorized")
}
})
api.get("/users", [list_users])
// Static files
app.static_("/static", "./public")
app.run("0.0.0.0:8080")
}// Single file with metadata
match ctx.form_file("avatar") {
Some(file) => {
println("Received: \{file.filename} (\{file.size} bytes)")
ctx.save_uploaded_file(file, "./uploads/" + file.filename)
}
None => ctx.abort_with_status(400, "No file uploaded")
}
// All multipart fields
let form = ctx.multipart_form()
for field_name, file in form {
println("\{field_name}: \{file.filename} (\{file.content_type})")
}
// Resumable upload (supports Content-Range)
ctx.save_uploaded_file_resumable(file, "./uploads/video.mp4")
// Chunked processing with progress callback
let mut progress = UploadProgress::new(file.size, 1024 * 1024)
ctx.save_uploaded_chunked(file, "./large.bin",
ChunkedWriteConfig::default(),
fn(chunk, i, total) {
progress = progress.update(chunk.length().to_int64())
println("Progress: \{progress.percentage.to_int()}%")
true // continue
})// Load from environment variables (APP_PORT, APP_DATABASE_URL, etc.)
let config = Config::from_env()
let port = config.get_int("port", default=8080L)
// Strongly-typed server config
let srv = ServerConfig::from_env()
app.set_max_multipart_memory(srv.max_upload_size)
// Check environment
if config.is_production() {
Logger::set_level(Info)
Logger::set_format(JSONFormat)
}| Method | Description |
|---|---|
| Mbit::default() | Create with logger + recovery |
| Mbit::new(engine) | Create with custom engine |
| get/post/put/delete/patch/head/options(path, handlers) | Route registration |
| any(path, handlers) | Match all methods |
| handle(methods, path, handlers) | Match specific methods |
| use(mw) | Add global middleware |
| group(prefix) | Create route group |
| no_route(handlers) | 404 handler |
| no_method(handlers) | 405 handler |
| static_file(path, file) | Serve single file |
| static_(prefix, root) | Serve directory |
| static_fs(prefix, root) | Serve directory with wildcard |
| set_html_template(name, content) | Register template |
| max_multipart_memory(bytes) | Set upload limit |
| health() | Register GET /health endpoint |
| redirect_trailing_slash(bool) | Auto-redirect trailing slashes |
| handle_method_not_allowed(bool) | Enable 405 responses |
| remove_extra_slash(bool) | Normalize double slashes |
| run(addr) | Start server |
| run_default() | Start on default port |
| run_with_shutdown(addr, on_shutdown) | Start with graceful shutdown |
| shutdown() | Trigger shutdown |
| routes() | List all routes |
| trees() | Print route tree |
| Method | Returns | Description |
|---|---|---|
| http_method() | String | HTTP method |
| path() | String | Request path |
| full_path() | String | Path with query string |
| param(key) | String? | Path parameter |
| param_default(key, default) | String | Path parameter with fallback |
| param_int(key) | Int? | Path parameter as Int |
| param_int64(key) | Int64? | Path parameter as Int64 |
| params_all() | Map[String,String] | All path params |
| query(key) | String? | Query parameter |
| query_default(key, default) | String | Query with fallback |
| query_int(key) | Int? | Query as Int |
| query_int64(key) | Int64? | Query as Int64 |
| query_map() | Map[String,String] | All query params |
| query_array(key) | Array[String] | Multi-value query |
| header(key) | String? | Request header |
| client_ip() | String | Client IP |
| content_type() | String | Content-Type |
| body_string() | String | Raw body |
| body_json() | Json? | Parsed JSON body |
| post_form(key) | String? | Form field |
| json(code, data) | Unit | Render JSON |
| xml(code, root, data) | Unit | Render XML |
| yaml(code, data) | Unit | Render YAML |
| html(code, html) | Unit | Render HTML |
| string(code, text) | Unit | Render plain text |
| data(code, contentType, body) | Unit | Render custom |
| redirect(code, url) | Unit | Redirect |
| file(path) | Unit | Serve file |
| sse(event) | Unit | SSE event |
| stream(code, writerFn) | Unit | Stream response |
| set_header(key, value) | Unit | Set response header |
| set(key, value) | Unit | Store value |
| get(key) | Json? | Get stored value |
| get_string(key) | String? | Get stored string |
| abort() | Unit | Stop handler chain |
| abort_with_status(code, msg) | Unit | Abort with JSON error |
| is_aborted() | Bool | Check if aborted |
| next() | Unit | Continue to next handler |
| must_bind_json::[T]() | T | Bind JSON or abort |
| negotiate_format(offers) | String? | Content negotiation |
| is_websocket() | Bool | WebSocket upgrade check |
type ChunkCallback = (String, Int, Int) -> Boolpub trait IRoutes {
fn get(String, Array[async (Context) -> Unit]) -> Unit
fn post(String, Array[async (Context) -> Unit]) -> Unit
fn put(String, Array[async (Context) -> Unit]) -> Unit
fn del(String, Array[async (Context) -> Unit]) -> Unit
fn patch(String, Array[async (Context) -> Unit]) -> Unit
fn head(String, Array[async (Context) -> Unit]) -> Unit
fn options(String, Array[async (Context) -> Unit]) -> Unit
fn any(String, Array[async (Context) -> Unit]) -> Unit
fn handle(Array[Method], String, Array[async (Context) -> Unit]) -> Unit
fn use_mw(async (Context) -> Unit) -> Unit
}let cfg = CORSConfig::with_origin_fn(fn(origin) {
origin.has_suffix(".mydomain.com") || origin == "https://app.mydomain.com"
})
app.use(cors(cfg))pub(all) struct ChunkedWriteConfig {
chunk_size : Int
process_callback : Bool
}pub(all) struct Config {
values : Map[String, ConfigValue]
env : AppEnvironment
env_prefix : String
}let cfg = Config::from_env()
let port = cfg.get_int("PORT", default=8080)pub(all) enum ConfigValue {
ConfigString(String)
ConfigInt(Int64)
ConfigBool(Bool)
ConfigFloat(Double)
} derive(Debug)pub(all) struct ContentRange {
start : Int64
end : Int64
total : Int64
}pub(all) struct Context {
req : Request
reader : &Reader
conn : ResponseConn
params : Map[String, String]
query_cache : Map[String, String]?
body : String?
post_form_cache : Map[String, String]?
handlers : Array[async (Context) -> Unit]
index : Int
status_code : Int
written : Bool
aborted : Bool
size : Int
store : Map[String, Json]
errors : Array[String]
headers : Map[String, String]
}let session = ctx.cookie("session_id")ctx.data_from_reader(200, content_length, "application/octet-stream", reader)ctx.file(200, "application/pdf", "./reports/annual.pdf")match ctx.form_file("avatar") {
Some(file) => {
println("Received: \{file.filename} (\{file.size} bytes)")
ctx.save_uploaded_file(file, "./uploads/" + file.filename)
}
None => ctx.abort_with_status(400, "No file uploaded")
}match ctx.negotiate_format(["application/json", "text/html"]) {
Some("application/json") => ctx.json(200, data)
Some("text/html") => ctx.html(200, "<h1>Hello</h1>")
_ => () // 406 already set
}async fn Context::save_uploaded_chunked(self : Context, file : FileHeader, dst : String, config : ChunkedWriteConfig, callback : (String, Int, Int) -> Bool) -> Unitlet mut bytes_written = 0L
ctx.save_uploaded_chunked(file, "./uploads/large.bin",
ChunkedWriteConfig::default(),
fn(chunk, index, total_chunks) {
bytes_written = bytes_written + chunk.length().to_int64()
println("chunk \{index}/\{total_chunks}")
true // continue
})match ctx.form_file("avatar") {
Some(file) => ctx.save_uploaded_file(file, "./uploads/" + file.filename)
None => ctx.abort_with_status(400, "No file uploaded")
}async fn Context::save_uploaded_file_resumable(self : Context, file : FileHeader, dst : String) -> Unitmatch ctx.form_file("video") {
Some(file) => {
ctx.save_uploaded_file_resumable(file, "./uploads/video.mp4")
}
None => ctx.abort_with_status(400, "No file")
}ctx.set_cookie("session_id", "abc123", max_age=3600, path="/", http_only=true)// Basic SSE
ctx.sse("message", "hello world")ctx.sse_keepalive("ping")ctx.stream(200, "text/event-stream", fn(write) {
for i = 0; i < 10; i = i + 1 {
write("data: chunk " + i.to_string() + "\n\n")
}
})ctx.string_f(200, "Hello {0}, you are {1} years old", ["Alice", "30"])ctx.xml(200, Json::object(H([
("user", Json::object(H([
("name", Json::string("Alice")),
("age", Json::string("30")),
]))),
])))
// Output: <root><user><name>Alice</name><age>30</age></user></root>pub(all) struct Engine {
router : Router
handle_method_not_allowed : Bool
redirect_trailing_slash : Bool
redirect_fixed_path : Bool
remove_extra_slash : Bool
use_raw_path : Bool
unescape_path_values : Bool
max_multipart_memory : Int64
remote_ip_headers : Array[String]
trusted_platform : String
forwarded_by_client_ip : Bool
trusted_proxies : Array[String]
active_server : Server?
}app.any("/api/health", [health_check])app.get("/api/articles", [list_articles])
app.get("/api/articles/:id", [auth, get_article]) // with middlewarelet api = app.group("/api")
api.use(auth_middleware)
api.get("/profile", [get_profile])app.handle([GET, POST], "/api/form", [handle_form])app.health()for route in app.routes() {
println("\{route.method} \{route.path}")
}app.run("127.0.0.1:3000")app.run_default()app.run_fd(3) // Listen on fd 3 passed by systemdfn Engine::run_listener(_self : Engine, _handler : async (Request, ServerConnection) -> Unit) -> Unitapp.run_listener(fn(request, reader, conn) {
// custom pre-processing
app.router.dispatch(request, reader, conn)
})app.run_tls("0.0.0.0:443", "./cert.pem", "./key.pem")let tls_config = TLSConfig::from_files("./cert.pem", "./key.pem")
|> with_auto_reload(interval=1800)
app.run_tls_with_config("0.0.0.0:443", tls_config)app.run_unix("/tmp/mbit.sock")app.no_route([not_found_handler])
app.run(addr)app.run_with_shutdown("0.0.0.0:8080")async fn Engine::serve_http(self : Engine, req : Request, reader : &Reader, conn : ServerConnection) -> Unit// In a health check or admin endpoint:
app.shutdown(cleanup=fn() {
println("Cleaning up resources...")
})app.static_file("/favicon.ico", "./assets/favicon.ico")app.static_files_engine("/assets", "./public")app.use(@mbit.logger())
app.use(@mbit.cors(@mbit.CORSConfig::default()))let rules = [FieldRules("username", [Required, Min(3), Max(20)]),
FieldRules("email", [Required, Email]),
FieldRules("age", [Min(0), Max(150)])]pub(all) struct FileHeader {
field_name : String
filename : String
content : String
content_type : String
size : Int64
}group.any("/health", [health_check])let api = app.group("/api/v1")
println(api.base_path()) // "/api/v1"let assets = app.group("/assets")
assets.static_file("/favicon.ico", "./public/favicon.ico")let assets = app.group("/assets")
assets.static("/css", "./public/css")let assets = app.group("/assets")
assets.static_fs("/css", "./public/css")pub(all) struct Logger {
}Logger::debug("Cache miss", fields=[("key", "user:42")])pub(all) enum MbitError {
BindError(String)
ValidationError(String)
NotFound(String)
InternalError(String)
} derive(Debug)fn MetricsCollector::record(self : MetricsCollector, status : Int, path : String, latency_ms : Int64) -> Unitpub(all) struct MultipartPartReader {
field_name : String
filename : String
content_type : String
remaining : Int64
pos : Int
body : String
boundary_marker : String
done : Bool
}let stream = MultipartStream::new(ctx)
match stream.next_part() {
Some(part) => {
loop {
match part.read_chunk(4096) {
Some(chunk) => { /* process chunk */ }
None => break
}
}
}
None => {}
}pub(all) struct MultipartStream {
body : String
boundary : String
pos : Int
done : Bool
}pub(all) struct RateLimitConfig {
max_requests : Int
window_secs : Int
strategy : RateLimitStrategy
token_bucket : RateTokenBucket
key_fn : (Context) -> String
condition : (Context) -> Bool
include_headers : Bool
}fn RateLimitConfig::with_key(self : RateLimitConfig, key_fn : (Context) -> String) -> RateLimitConfigpub(all) struct RateTokenBucket {
rate : Double
burst : Int
}async fn ResponseConn::send_response(self : ResponseConn, code : Int, reason : String, extra_headers? : Map[String, String]) -> Unitpub(all) struct RouteInfo {
meth : String
path : String
handler_count : Int
}router.any("/api/health", [health_check])async fn Router::dispatch(self : Router, req : Request, reader : &Reader, conn : ServerConnection) -> Boollet routes = router.routes()
for route in routes {
println("\{route.method} \{route.path}")
}pub(all) enum Rule {
Required
Min(Int)
Max(Int)
Len(Int)
Pattern(String)
OneOf(Array[String])
Email
URL
Custom((String) -> String?)
}pub(all) struct SecureConfig {
frame_options : String
content_type_nosniff : Bool
xss_protection : Bool
content_security_policy : String
referrer_policy : String
strict_transport_security : String
permissions_policy : String
cross_origin_opener_policy : String
is_api : Bool
skip_hsts : Bool
enable_csrf : Bool
csrf_secret : String
csrf_cookie_name : String
csrf_header_name : String
csrf_form_name : String
safe_methods : Array[String]
csrf_exclude_paths : Array[String]
csrf_token_expiry : Int
}pub(all) struct ServerConfig {
host : String
port : Int
database_url : String
redis_url : String
log_level : String
log_format : String
secret_key : String
cors_origins : Array[String]
max_upload_size : Int64
rate_limit : Int
rate_limit_window : Int
tls_cert_file : String
tls_key_file : String
environment : AppEnvironment
debug : Bool
}pub(all) struct TLSConfig {
cert_file : String
key_file : String
min_version : String
auto_reload : Bool
reload_interval : Int
cipher_suites : Array[String]
}pub(all) struct TemplateStore {
templates : Map[String, String]
delims_left : String
delims_right : String
}fn TemplateStore::render(self : TemplateStore, name : String, data : Map[String, String]) -> String?pub(all) struct TestConn {
status : Int
reason : String
headers : Map[String, String]
body : String
header_sent : Bool
ended : Bool
}pub(all) struct UploadProgress {
total_size : Int64
bytes_processed : Int64
percentage : Double
current_chunk : Int
total_chunks : Int
}pub(all) enum WebSocketMessage {
Text(String)
Binary(String)
Close(Int, String)
Ping(String)
Pong(String)
} derive(Debug)let accounts = Map([("admin", "secret")])
app.use(basic_auth_for_realm(accounts, "My Realm"))struct ContactForm {
name : String
email : String
message : String
} derive(FromJson)
let form : ContactForm? = bind_form(ctx)struct Headers {
content_type : String
authorization : String
} derive(FromJson)
let hdrs : Headers? = bind_header(ctx)struct LoginRequest {
username : String
password : String
} derive(FromJson)
let req : LoginRequest? = bind_json(ctx)struct ArticleParams {
id : String
slug : String
} derive(FromJson)
// Route: /article/:id/:slug
let params : ArticleParams? = bind_path(ctx)app.use(body_size_limit(1024 * 1024)) // 1 MBfn clean_path(path : String) -> Stringlet ctx = create_test_context("GET", "/test")
assert_eq(ctx.method(), "GET")fn debug_print(msg : String) -> Unitfn debug_print_default() -> Unitfn debug_print_route(meth : String, path : String) -> Unitfn debug_print_warning(msg : String) -> Unitasync fn dir_listing(dir_path : String) -> Stringctx.html(200, dir_listing("./public"))fn disable_bind_validation() -> Unitfn enable_bind_validation() -> Unitfn guess_content_type(path : String) -> Stringapp.use(gzip(GzipConfig::default()))ctx.json(200, Json::object(h([
("message", Json::string("hello")),
("code", Json::number(200.0)),
])))fn is_bind_validation_enabled() -> Boolasync fn load_html_glob(pattern : String) -> Unitapp.use(logger())let metrics = MetricsCollector::new()
app.use(metrics_middleware(metrics))
app.get("/metrics", [fn(ctx) { ctx.json(200, metrics.to_json()) }])fn name_and_version() -> Stringlet range = parse_content_range("bytes 0-499/1024")
assert_eq(range.start, 0)
assert_eq(range.end, 499)
assert_eq(range.total, 1024)app.use(recovery())fn search(node : Node, segments : Array[String], idx : Int, params : Map[String, String]) -> MatchResult?// Web app
app.use(secure(SecureConfig::default()))
// With CSRF protection
app.use(secure(SecureConfig::default().with_csrf()))
// API server
app.use(secure(SecureConfig::api_default()))fn set_html_template(name : String, content : String) -> Unitfn set_template_delims(left : String, right : String) -> Unitapp.use(static_files("/static", "./public"))fn status_text(code : Int) -> Stringapp.use(structured_logger(StructuredLogConfig::default()))fn[T] validate(_value : T) -> String?let values = Map([("username", "alice"), ("email", "alice@example.com")])
let rules = [
FieldRules::new("username", [Required, Min(3)]),
FieldRules::new("email", [Required, Email]),
]
let errors = validate_map(values, rules)
if errors.has_errors() {
ctx.json(400, errors.to_json())
return
}async fn validate_or_abort(ctx : Context, values : Map[String, String], rules : Array[FieldRules]) -> Boollet values = Map([("username", "alice")])
let rules = [FieldRules::new("username", [Required, Min(3)])]
if !validate_or_abort(ctx, values, rules) {
return
}
// Proceed with valid data...app.get("/ws", [websocket_handler(fn(ws) {
ws.send_text("Hello via WebSocket!")
loop {
match ws.read() {
Some(WebSocketMessage::Text(t)) => ws.send_text("Echo: " + t)
Some(WebSocketMessage::Close(_, _)) => break
_ => ()
}
}
})])Install
Download zipA Gin-inspired web framework for MoonBit — fast, lightweight, idiomatic.