Examples

Hello World

using Mongoose

router = Router()
route!(router, :get, "/", req -> text("Hello, World!"))

app = App(; router=router)
start!(app; port=8080)

HTTPS Server (TLS)

using Mongoose

router = Router()
route!(router, :get, "/secure", req -> text("secure ok"))

app = App(; router=router, tls=TLSConfig(cert="path/to/cert.pem", key="path/to/key.pem"))
start!(app; port=8443)

REST API with Typed Parameters

using Mongoose

router = Router()

# String parameter (default)
route!(router, :get, "/greet/:name", (req, name) -> text("Hello, $name!"))

# Typed integer — /users/abc returns 404 automatically
route!(router, :get, "/users/:id::Int", (req, id) ->
    json(Dict("id" => id, "type" => string(typeof(id))))
)

# Float parameter
route!(router, :get, "/price/:amount::Float64", (req, amount) -> begin
    tax = amount * 0.16
    json(Dict("amount" => amount, "tax" => tax))
end)

# Wildcard catch-all
route!(router, :get, "/files/*path", (req, path) ->
    text("Requested: $path")
)

app = App(; router=router, workers=4)
start!(app; port=8080)

Frozen Router (Compiled Dispatch)

Once all routes are registered, freeze!(router) closes the table and compiles it: each route's terminal (handler + scoped middleware) is pre-baked with the handler's concrete type captured, and parametric matching walks the raw path with byte indices — no per-request path split, no closure/concat allocation. Dispatch semantics stay identical; only registration is locked.

using Mongoose

router = Router()

get!(router, "/users/:id::Int", (req, id) -> json(Dict("id" => id)))
post!(router, "/users", req -> json(Dict("created" => true); status=201))
get!(router, "/files/*path", (req, path) -> text("Requested: $path"))

freeze!(router)   # route!/ws! throw RouteError from here on

app = App(; router=router, workers=4)
start!(app; port=8080)

Freezing also provides the closed-table guarantee AOT builds need: with no runtime registration, the route table can be compiled once and pruned. Note that juliac --trim compatibility is not complete yet — the trim verifier still finds dynamic dispatch in startup/registration. Call freeze! after the last registration and before starting the app.

Query Parameters

Use the query() helper for type-safe access with automatic parsing:

using Mongoose

router = Router()

route!(router, :get, "/search", req -> begin
    q     = query(req, "q", "")       # String default
    page  = query(req, "page", 1)     # Auto-parsed to Int
    limit = query(req, "limit", 10)   # Auto-parsed to Int
    active = query(req, "active", true)  # Auto-parsed to Bool

    json(Dict("query" => q, "page" => page, "limit" => limit, "active" => active))
end)

app = App(; router=router, workers=4)
start!(app; port=8080)

JSON Request and Response

JSON is built-in via JSON — no setup required:

using Mongoose

router = Router()

# Return JSON responses using json() helper
route!(router, :get, "/users/:id::Int", (req, id) ->
    json(Dict("id" => id, "name" => "User $id", "active" => true))
)

# Parse JSON request body with json()
route!(router, :post, "/users", req -> begin
    data = parsejson(req)  # returns Dict/Array from JSON
    json(Dict("created" => data["name"]); status=201)
end)

# Typed request validation into a struct
struct CreateUser
    name::String
    email::String
    age::Int
end

route!(router, :post, "/users/typed", req -> begin
    user = validate(req, CreateUser)  # parses + validates; throws ValidationError
    json(Dict("name" => user.name, "email" => user.email))
end)

app = App(; router=router, workers=4)
start!(app; port=8080)

Multipart File Upload

using Mongoose

router = Router()

route!(router, :post, "/upload", req -> begin
    parts = parsemultipart(req)  # Dict{String, Union{String, MultipartFile}}
    isempty(parts) && return json(Dict("error" => "No files"); status=400)

    for (name, value) in parts
        # value is a String (form field) or MultipartFile (uploaded file)
    end

    file = parts["avatar"]::MultipartFile
    json(Dict("files" => [file.filename], "size" => length(file.data)); status=201)
end)

app = App(; router=router, workers=4, max_body_bytes=10_000_000)  # 10MB limit
start!(app; port=8080)

Middleware Stack

using Mongoose

router = Router()
route!(router, :get, "/", req -> json(Dict("status" => "ok")))
route!(router, :get, "/api/data", req -> json(Dict("data" => [1,2,3])))

app = App(; router=router, workers=4)

# Middleware runs in registration order
use!(app, security())                                        # Security headers
use!(app, health())                                          # /healthz, /readyz, /livez
use!(app, metrics())                                         # GET /metrics
use!(app, cors(origins="*"))                                 # CORS headers
use!(app, compress(min_size_bytes=1024))                           # GZip compression
use!(app, logger())                                          # Access logs
use!(app, ratelimit(max_requests=100, window_seconds=60))    # Rate limiting
use!(app, bearer(t -> t == "secret"); paths=["/api"])        # Auth on /api only

start!(app; port=8080)

Custom Middleware

using Mongoose

struct RequestTimer <: Mongoose.AbstractMiddleware end

function (::RequestTimer)(req::Request, next::Function)
    t = time()
    res = next()
    elapsed = round((time() - t) * 1000, digits=1)
    @info "$(req.method) $(req.uri)" status=res.status ms=elapsed
    return res
end

router = Router()
route!(router, :get, "/", req -> text("hello"))

app = App(; router=router, workers=4)
use!(app, RequestTimer())
start!(app; port=8080)

Route Groups

using Mongoose

router = Router()

# Public routes
route!(router, :get, "/", req -> json(Dict("msg" => "welcome")))

# API group with scoped middleware
api = group("/api/v1", middleware=[
    ratelimit(max_requests=100, window_seconds=60),
    apikey(header_name="x-api-key", keys=Set(["my-key"])),
])

route!(api, :get, "/users", req -> json(Dict("users" => [])))
route!(api, :post, "/users", req -> begin
    data = parsejson(req)
    json(Dict("created" => data["name"]); status=201)
end)
route!(api, :get, "/users/:id::Int", (req, id) -> json(Dict("id" => id)))

# Nested admin group
group!(api, "/admin", middleware=[bearer(t -> t == "admin-secret")]) do admin
    route!(admin, :delete, "/users/:id::Int", (req, id) ->
        json(Dict("deleted" => id))
    )
end

mount!(router, api)

app = App(; router=router, workers=4)
start!(app; port=8080)

WebSocket

using Mongoose

router = Router()

route!(router, :get, "/", req -> html("""
    <script>
    const ws = new WebSocket("ws://localhost:8080/ws");
    ws.onmessage = e => console.log(e.data);
    ws.on_open = () => ws.send("hello");
    </script>
    <p>Check console</p>
"""))

ws!(router, "/ws";
    on_open = req -> begin
        auth = get(req.headers, "authorization", nothing)
        # Return false to reject with 403
        @info "WS connected" uri=req.uri
        true
    end,
    on_message = msg -> Message("Echo: $(msg.data)"),
    on_close = () -> @info "WS disconnected"
)

app = App(; router=router, workers=4, ws_idle_timeout_ms=60_000)
start!(app; port=8080)

WebSocket Server Push (broadcastws)

WebSockets can also be pushed to: send a frame to every open client of a path from any task (background housekeeping, event relays, request handlers). Frames route through the same thread-safe reply queue the worker pool uses and are actually sent on the poll thread, so no C connection is ever touched from another thread.

using Mongoose
import JSON

router = Router()

# Clients subscribe to stock updates
ws!(router, "/stock";
    on_message = msg -> Message("pong: $(msg.data)"),
)

app = App(; router=router, workers=4)

# Broadcast a stock event to everyone currently connected to /stock
broadcastws(app, "/stock", JSON.json(Dict("event" => "low", "sku" => "SHOP-MUG-6")))

start!(app; port=8080)

Requires an async executor (workers > 0); stale/closed connections are dropped silently.

Server-Sent Events (SSE)

using Mongoose

router = Router()

route!(router, :get, "/events", req ->
    sse(req) do writer
        for i in 1:10
            emit(writer; data="Tick $i", event="heartbeat", id=string(i))
            sleep(1)
        end
        emit(writer; data="done", event="close")
    end
)

route!(router, :get, "/", req -> html("""
    <script>
    const es = new EventSource("/events");
    es.addEventListener("heartbeat", e => console.log(e.data));
    es.addEventListener("close", e => { console.log("done"); es.close(); });
    </script>
    <p>Check console for SSE events</p>
"""))

app = App(; router=router, workers=4)
start!(app; port=8080)

Static File Serving

using Mongoose

router = Router()
route!(router, :get, "/", req -> redirect("/static/index.html"))

app = App(; router=router, workers=4)

# Serve files from "public/" directory at /static/* prefix
# Supports Range requests, ETag, and gzip (handled at C level)
serve!(app, "public"; uri_prefix="/static")

start!(app; port=8080)

Cookies

using Mongoose

router = Router()

route!(router, :post, "/login", req -> begin
    c = Cookie("session", "abc123";
        httponly = true,
        secure   = true,
        max_age  = 3600,
        samesite = :strict,
    )
    setcookie(c)  # serialize to a Set-Cookie header value; add it as a response header:
    json(Dict("logged_in" => true); headers=["Set-Cookie" => setcookie(c)])
end)

route!(router, :get, "/profile", req -> begin
    jar = parsecookies(req)
    session = get(jar, "session", nothing)
    session === nothing && return json(Dict("error" => "unauthorized"); status=401)
    json(Dict("session" => session))
end)

app = App(; router=router, workers=4)
start!(app; port=8080)

Custom Error Responses

using Mongoose

router = Router()
route!(router, :get, "/", req -> json(Dict("ok" => true)))

app = App(; router=router, workers=4)

# Static error responses
onerror!(app, 500, json(Dict("error" => "Internal Server Error"); status=500))
onerror!(app, 413, json(Dict("error" => "Payload too large"); status=413))

# Dynamic error handler (handler receives the request)
onerror!(app, 404) do req
    json(Dict("error" => "Not found", "path" => req.uri); status=404)
end

start!(app; port=8080)

Dependency Injection

using Mongoose

# Example: share a database connection
struct FakeDB
    users::Dict{Int,String}
end

router = Router()

route!(router, :get, "/users/:id::Int", (req, id) -> begin
    db = withservices(req) do svcs
        svcs.db                       # concrete type → type-stable
    end
    name = get(db.users, id, nothing)
    name === nothing && return json(Dict("error" => "not found"); status=404)
    json(Dict("id" => id, "name" => name))
end)

app = App(; router=router, workers=4)
service!(app, :db, FakeDB(Dict(1 => "Alice", 2 => "Bob")))

start!(app; port=8080)

Background Tasks & Lifecycle Hooks

using Mongoose

router = Router()
route!(router, :get, "/", req -> text("running"))

app = App(; router=router, workers=4)

onstart!(app) do
    @info "Server started, seeding data..."
end

onstop!(app) do
    @info "Graceful shutdown complete"
end

background!(app) do
    while true
        @info "Background tick" time=time()
        sleep(30)
    end
end

start!(app; port=8080)

GZip Compression

using Mongoose

router = Router()

# Large response that benefits from compression
route!(router, :get, "/data", req -> begin
    large_data = Dict("items" => [Dict("id" => i, "value" => "x"^100) for i in 1:100])
    json(large_data)
end)

app = App(; router=router, workers=4)

# Compress responses larger than 1KB when client accepts gzip
use!(app, compress(min_size_bytes=1024))

start!(app; port=8080)

Testing with FakeTransport

FakeTransport is the FFI-free reference transport: it dispatches requests through the full pipeline with no server and no Mongoose_jll, so tests never bind a port.

using Test, Mongoose

# Setup
router = Router()
route!(router, :get, "/hello", req -> json(Dict("msg" => "hi")))
route!(router, :post, "/echo", req -> begin
    data = parsejson(req)
    json(data; status=201)
end)

app = App(; router=router)
use!(app, cors())

client = FakeTransport(app)   # or: FakeTransport(app)

# Test GET
resp = client(:get, "/hello")
@test resp.status == 200
@test contains(resp.body, "\"msg\"")

# Test POST with JSON body
resp = client(:post, "/echo";
    body = """{"name": "Alice"}""",
    headers = ["Content-Type" => "application/json"],
)
@test resp.status == 201
@test contains(resp.body, "Alice")

# Test with query parameters
resp = client(:get, "/hello"; query=Dict("foo" => "bar"))
@test resp.status == 200

For deterministic async tests, FakeExecutor mirrors the AsyncExecutor contract (queued submit!, haspending, start!/stop!) but never spawns workers: jobs run only when the test calls run!, inline in submission order — no threads, no sleeps.

fe = FakeExecutor()
@test submit!(fe, () -> "first") == true   # enqueued, not run
@test haspending(fe)
@test run!(fe) == ["first"]                # FIFO, inline
@test !haspending(fe)

Production Configuration

using Mongoose

router = Router()
# ... define routes ...

app = App(;
    router          = router,
    workers         = parse(Int, get(ENV, "WORKERS", "4")),
    queue_size       = 2048,
    max_body_bytes        = 4_000_000,       # 4MB
    request_timeout_ms = 30_000,          # 30s
    drain_timeout_ms   = 10_000,          # 10s graceful shutdown
    ws_idle_timeout_ms = 120_000,         # 2min WS idle
    header_timeout_ms  = 10_000,          # close conns that stall before headers
    body_timeout_ms    = 30_000,          # max time to receive a request body
    max_header_bytes   = 64 * 1024,       # request-header cap
    send_buffer_bytes  = 1_048_576,       # unsent bytes/conn (streams + WS)
    max_bg_tasks       = 0,               # 0 = auto (4×workers)
    max_connections    = 10_000,          # refuse beyond this many open conns
)

# Full middleware stack
use!(app, security())
use!(app, health(ready_check = () -> true))
use!(app, metrics())
use!(app, cors(origins=get(ENV, "CORS_ORIGINS", "*")))
use!(app, compress(min_size_bytes=1024))
use!(app, logger())

# Error responses
onerror!(app, 500, json(Dict("error" => "Internal error"); status=500))
onerror!(app, 413, json(Dict("error" => "Too large"); status=413))
onerror!(app, 503, json(Dict("error" => "Overloaded"); status=503))

# Services
service!(app, :env, get(ENV, "APP_ENV", "production"))

# Static assets
serve!(app, "public"; uri_prefix="/static")

start!(app; host="0.0.0.0", port=parse(Int, get(ENV, "PORT", "8080")))