Production-ready HTTP & WebSocket framework for Julia
Powered by the battle-tested Mongoose C library
- β‘ Fast β precompiled cold start;
freeze!compiles the route table so warm dispatch stays at ~100 ns (fixed routes) / ~400 ns (typed params) with minimal allocation. - π§ͺ Testable without FFI β
FakeTransportdrives the full pipeline in pure Julia: no ports, no C library, deterministic tests. - π§© Batteries included β CORS, rate limiting, bearer/API-key/basic auth, gzip, ETag, security headers, structured logs, Prometheus metrics, health checks, static files.
- π‘ Real-time built in β WebSocket (including server-initiated push) and Server-Sent Events on the same port, with backpressure and graceful drain.
- π Replaceable parts β router, executor, and transport sit behind small protocols; the core has no FFI dependency and can be swapped or embedded.
- π‘οΈ Production defaults β graceful shutdown on SIGINT and SIGTERM, request timeouts, header timeouts, connection caps, 503 backpressure, typed error handlers, dependency injection.
using Pkg; Pkg.add("Mongoose")using Mongoose
app = App(workers=4)
get!(app, "/") do req
text("Hello from Mongoose.jl!")
end
get!(app, "/users/:id::Int") do req, id
json(Dict("id" => id, "name" => "User $id"))
end
post!(app, "/echo") do req
json(parsejson(req); status=201)
end
start!(app; port=8080)get!(app, "/items", req -> ...)
post!(app, "/items", req -> ...)
put!(app, "/items/:id", (req, id) -> ...)
patch!(app, "/items/:id", (req, id) -> ...)
delete!(app, "/items/:id", (req, id) -> ...)
route!(app, :get, "/search", req -> ...)Typed path parameters use :name::Type and arrive as a typed tuple (invalid
values are 404s). Wildcards capture the rest of the path:
get!(app, "/users/:id::Int", (req, id) -> ...) # id::Int
get!(app, "/price/:val::Float64", (req, val) -> ...) # val::Float64
get!(app, "/posts/:slug", (req, slug) -> ...) # slug::String
get!(app, "/files/*path", (req, path) -> ...) # wildcardGroups share a prefix and scoped middleware (composed as
global β group β route at dispatch time):
api = group("/api/v1", middleware=[
ratelimit(max_requests=100, window_seconds=60),
apikey("key-abc"; header_name="x-api-key"),
])
get!(api, "/users", list_users)
post!(api, "/users", create_user)
group!(api, "/admin", middleware=[bearer("admin-token")]) do admin
delete!(admin, "/users/:id::Int", delete_user)
end
mount!(app, api)Compiled dispatch β register everything, then freeze! to close and compile
the route table (later registration throws). The closed table is the foundation
for AOT builds; juliac --trim compatibility is still in progress:
freeze!(app) # or freeze!(router) before App(router=router)Any callable (req, next) β Response is middleware β no subtyping needed.
Built-ins cover the common production stack:
use!(app, security()) # OWASP headers
use!(app, health()) # /healthz /readyz /livez
use!(app, metrics()) # Prometheus /metrics
use!(app, cors(origins="https://myapp.com")) # CORS + preflight
use!(app, compress(min_size_bytes=1024)) # gzip
use!(app, etag()) # ETag + 304/412
use!(app, logger()) # access logs
use!(app, ratelimit(max_requests=100, window_seconds=60))
use!(app, bearer("secret"); paths=["/api"]) # path-scoped auth
use!(app, apikey(["key-abc", "key-xyz"]))
use!(app, basicauth("admin", ENV["ADMIN_PASSWORD"]))
serve!(app, "public"; uri_prefix="/static") # C-level static filesCustom middleware β a closure or a small type:
use!(app) do req, next
t = time()
res = next()
@info "$(req.method) $(req.uri)" status=res.status ms=round((time()-t)*1000; digits=1)
res
endbody(req) # raw body
parsejson(req) # JSON β Dict/Array/...
parseform(req) # urlencoded body β Dict
parsemultipart(req) # multipart body β Dict/MultipartFile
parsecookies(req) # Cookie header β Dict
parsequery(req) # whole query Dict (query(req, k) for typed lookups)
validate(req, CreateUser) # parse + coerce + validate into a struct
query(req, "page", 1) # typed query param with default
header(req, "authorization")
context(req) # per-request Dict{Symbol,Any}json(Dict("ok" => true)) # 200, application/json
json(data; status=201, headers=["X-H" => "v"])
text("Hello!") # text/plain
html("<h1>Hi</h1>")
redirect("/new"; status=301)
Response(Json, """{"raw":true}""") # pre-serialized bodyErrors can be thrown or handled by status/type:
throw(NotFoundError("user 7")) # β 404 automatically
onerror!(app, 404) do req
json(Dict("error" => "not found"); status=404)
end
onerror!(app, AccountGone) do req, e # typed exception handler
json(Dict("error" => "gone"); status=410)
endws!(app, "/chat";
allowed_origins = ["https://myapp.com"], # optional
on_open = req -> true, # false β 403
on_message = msg -> Message("Echo: $(msg.data)"),
on_close = () -> @info "disconnected",
)
# Server-initiated push to every client of a path (thread-safe, any task):
broadcastws(app, "/chat", "Server announcement")Slow WS readers are protected too: pushes beyond
send_buffer_bytes(default 1 MiB) unsent are dropped (not buffered), andmongoose_ws_frames_droppedcounts them β clients resync on reconnect.Note: unmasked client frames are tolerated (Mongoose's parser does not enforce RFC 6455 Β§5.1 masking). Browsers always mask and there is no server-side security impact.
get!(app, "/events") do req
sse(req) do writer
for i in 1:10
emit(writer; data="Tick $i", event="update", id=string(i))
sleep(1)
end
end
endapp = App(;
workers = 4, # 0 = sync (inline); N = worker pool
queue_size = 1024, # pending requests before 503
request_timeout_ms = 5_000, # 0 = disabled (async mode)
drain_timeout_ms = 5_000, # graceful shutdown budget
max_body_bytes = 1_048_576,
header_timeout_ms = 0, # close conns that stall before headers
body_timeout_ms = 0, # 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); runaway cap
max_connections = 0, # 0 = unlimited
ws_max_frame_bytes = 1_048_576,
ws_idle_timeout_ms = 0, # 0 = disabled
tls = nothing, # TLSConfig(cert=β¦, key=β¦)
)Units are explicit in every name: _ms for timeouts, _seconds for protocol
durations, _bytes for sizes. Optional values are disabled with nothing.
HTTPS is native:
app = App(; tls = TLSConfig(cert="path/to/cert.pem", key="path/to/key.pem"))
start!(app; port=8443)- Graceful shutdown β SIGINT and SIGTERM drain in-flight requests and SSE
streams, run
onstop!hooks, and stop workers (SIGTERM and normal exits go through Julia'satexitpath; SIGINT is caught whilestart!blocks). - Backpressure β the async executor bounds its queue and answers
503when full;max_connections,header_timeout_ms,body_timeout_ms, andmax_header_bytesprotect against slow clients and oversized requests. - Observability β
logger()access logs,metrics()(request counters, latency histogram, and live gauges: connections, WS clients, streams, executor depth), andhealth()probes for Kubernetes. - Errors β
onerror!per status or per exception type; typedHTTPError{status}aliases map to responses automatically. - Introspection β
isrunning(app),url(app),length(app),matchroute(app, β¦),hasroute(app, path).
onstart!(app) do; @info "starting"; connect_database!() end
onstop!(app) do; @info "stopping"; close_database!() end
background!(app) do
while true
cleanup_expired_sessions!()
sleep(60)
end
endDependency injection is a typed NamedTuple; withservices gives type-stable
access inside the closure:
app = App(; services=(db=connect_to_database(), cache=RedisPool()))
get!(app, "/users") do req
users = withservices(req) do svcs
fetch_users(svcs.db)
end
json(users)
endFakeTransport runs the full pipeline β middleware, routing, serialization β
with no network and no FFI:
using Test, Mongoose
app = App()
get!(app, "/hello", req -> json(Dict("msg" => "hi")))
use!(app, cors())
client = FakeTransport(app)
resp = client(:get, "/hello")
@test resp.status == 200
@test contains(resp.body, "\"msg\"")For deterministic async tests, FakeExecutor queues jobs on submit! and
runs them only when you call run!, in submission order:
fe = FakeExecutor()
submit!(fe, () -> "work") # enqueued, not run
@test run!(fe) == ["work"] # FIFO, inline, no threadsEach boundary is a replacement point: App(router=my_router),
App(workers=n) chooses the executor, and AbstractTransport declares its
capabilities (canws, cantls, canstream). Custom routers
implement route!/matchroute/hasroute and may carry their own endpoint
type via invokeendpoint. See the API reference for the exact contracts.
- API reference: AbrJA.github.io/Mongoose.jl
- Examples: docs/src/examples.md β REST, auth, streaming, WebSocket, middleware stacks, deployment patterns.
- Changelog: CHANGELOG.md
Distributed under the GPL-2 License. See LICENSE for details.