If you've written a Node.js API with Express, BoxExpress will feel immediately
familiar — same app.get()/app.use() shape, same middleware chain, same
req/res conventions. The difference is what's underneath: BoxExpress runs
on BoxLang and Undertow, with a
virtual thread per request, as a standalone server with no servlet container
required.
app = boxExpress()
app.get( "/", ( req, res ) => {
res.send( "Hello World" )
} )
app.listen( 3000, ( port ) => {
println( "listening on #port#" )
} )
Install it from ForgeBox:
box install boxlang-express
Below is the full feature list as it stands today.
Routing & the App
app.get/post/put/patch/delete/head/all(path, ...handlers) — standard verb
routing, with variadic middleware chains per route.
app.use(handler) / app.use(path, handler) / app.use(path, router) —
also accepts multiple handlers (or a mix of handlers and a mountable
Router) in one call.
app.param(name, callback) — centralize a lookup/validation step for any
route capturing that param name.
app.route(path) — a chainable per-path builder (.get().post()...) so
multiple methods on the same path don't repeat it.
Router — mountable sub-apps (boxExpressRouter()), same routing methods
as app, mounted with app.use("/api", apiRouter).
- Path matching: case-insensitive literal segments,
:params, a trailing
optional :name?, and a trailing * wildcard — both * and :name? throw
at registration time if used anywhere but the final segment, rather than
silently matching more or less than expected.
app.get() routes automatically answer HEAD too (body discarded, headers
intact), matching Express.
app.set(name, value) / app.getSetting(name), app.locals (merged into
every render, alongside res.locals).
app.listen(port, callback, options) — blocks the calling thread by
default (no manual keep-alive loop needed, unlike Node); { block: false }
opts back into non-blocking. options.backlog tunes the TCP accept queue
(default 1024).
app.close() — stops the server, safely callable from a different thread
than the one blocked in listen().
app.getConnectorStatistics() — live HTTP-layer metrics straight from
Undertow (active connections, request counts, bytes sent/received, error
count, processing time).
- Colored access logging on stdout for every request, toggleable with
app.set("log", false). Respects NO_COLOR.
app.set("reloadOnChange", true) — dev-only auto-restart on .bx/.bxs
file changes, replaying the original process launch.
Request & Response
req: method, path, originalUrl, query, params, headers,
get(name), cookies, ip, protocol, secure, hostname, body,
rawExchange() (escape hatch to the underlying Undertow exchange).
app.set("trust proxy", true) gates X-Forwarded-For/-Proto/-Host
trust; a named header list (app.set("trust proxy header", ...)) supports
platforms (DigitalOcean, Cloudflare) where forwarded headers are
append-only and otherwise spoofable.
res: status(), set()/header(), type(), send(), json(),
sendStatus(), redirect(), cookie(), sendBytes(), sendFile(),
download(), end(), render(), dump() (BoxLang's rich HTML dump() as
a debug escape hatch), getStatusCode()/getBytesWritten(),
onBeforeSend(callback).
- Calling a second terminal response method throws — mirrors Express's
"headers already sent."
- File serving:
sendFile()/download()/static serving all set
ETag/Last-Modified, honor conditional If-None-Match/If-Modified-Since
(304s), support options.maxAge for Cache-Control, and contain
user-supplied paths to a configured root.
- Range requests (RFC 7233):
206 Partial Content for a Range header,
416 for an unsatisfiable one, Accept-Ranges: bytes on every file
response — enables video/audio scrubbing and resumable downloads.
- Server-Sent Events (
res.sse): long-lived one-way event streams.
emitter.send(data, event, id), emitter.comment(text), emitter.close(),
emitter.isClosed(). Thread-safe — an emitter can be stashed and pushed to
from an entirely different route for broadcast/fan-out. Newline-stripped
fields to prevent event injection.
Views
- Two rendering engines, picked by extension:
.bxm (BoxLang's native
server-page format, <bx:output>) and .hbs (Handlebars, via bundled
handlebars.java).
app.set("views", dir), app.set("view engine", "hbs") for a default
extension.
app.locals / res.locals merge into every render() call
(app.locals < res.locals < explicit data, matching Express's
precedence).
- Path traversal protection: the resolved view is checked against the views
directory's real path before either engine touches the file.
WebSockets & Real-Time
app.ws(path, callback) — WebSocket routes, separate from the
HTTP method chain. connection.send(), onMessage(), onClose(),
isClosed(), plus handshake headers/cookies for auth. Built on a small
vendored piece of compiled Java (the only compiled Java in the project) —
Undertow's own WebSocket extension points aren't reachable from BoxLang
directly.
- STOMP 1.2 broker (
boxExpressStomp()) — a full pub/sub broker on top of
app.ws():
SUBSCRIBE/SEND, authenticate()/authorize() hooks with
per-connection metadata echoed back as CONNECTED headers.
- Receipts, full
ACK/NACK (auto/client/client-individual modes),
BEGIN/COMMIT/ABORT transactions.
- Bidirectional heartbeat negotiation with connection monitoring.
Sec-WebSocket-Protocol negotiation for stomp.js and other client
libraries.
- Exchanges & bindings — AMQP-style routing:
direct, topic
(wildcard */# matching), fanout, distribution (random/round-robin),
and a custom-exchange escape hatch.
- A live connection registry (
getConnections(), getConnectionDetails())
and server-side listeners that react to a destination from plain server
code, no WebSocket connection required.
- Header values escaped per spec (not just stripped) to prevent frame
injection.
- Cluster relay (
options.cluster) — see Cluster
Support below: relays a publish to every other
instance in a cluster, not just this process's own local subscribers.
Scheduler
app.schedule(intervalMs, callback, options) — fixed-interval
recurring jobs, the setInterval equivalent. One shared
ScheduledExecutorService drives ticks; each tick's callback runs on its
own virtual thread, so a slow job never delays another job's tick.
options.name, options.immediate (run first tick right away), and
options.allowOverlap (default false — an overlapping tick is skipped,
not queued).
options.clustered (default false) — see Cluster
Support below: runs the job on exactly one elected
instance across a cluster, instead of once per instance, with passive
failover if that instance goes down.
- Errors inside a job are caught and logged, never left to kill future ticks.
app.getScheduledJobs() for live introspection; app.close() shuts the
scheduler down cleanly.
- Deliberately no cron-expression parsing and no persistence across
restarts.
Cluster Support
app.getClusterManager() — lazily builds one shared ClusterManager
per app: cross-process peer discovery and manager election, backed by a
durable, shared cache() rather than static config, so instances never
need to know each other's addresses in advance — each one registers its
own identity and reads everyone else's back from the same cache.
- Two consumers built on top of it:
app.schedule(intervalMs, callback, { clustered: true }) — only
the elected instance runs a clustered job's tick; every other instance
does nothing for it. Failover is passive: if the elected instance goes
down, the next instance to check finds its heartbeat stale and
promotes itself.
boxExpressStomp({ cluster: app.getClusterManager() }) — opens a
mesh of outbound WebSocket connections to every live peer; a publish
that would otherwise only reach local subscribers is also relayed to
every other instance, delivered there through that instance's own
normal subscriber/exchange/listener path. A message that arrives via
the relay is never relayed back out, so it can't loop.
- An app that never opts in (
cluster.enabled stays false, the default)
pays nothing for any of this — every ClusterManager method becomes a
no-op matching today's unclustered behavior exactly.
- Configured via
boxlang.json's modules.boxexpress.settings.cluster, or
app.set("cluster", {...}): name (this instance's own address, e.g.
ws://10.0.1.4:3000, typically resolved at deploy time via
${env.POD_IP}-style interpolation rather than hardcoded), cacheProvider
(required once enabled), secretKey (gates the relay's /__cluster
endpoint), peerIdleTimeoutSeconds (default 30).
cacheProvider must name a cache backed by a genuinely durable/shared
object store — validated at startup against BoxLang's own
IObjectStore.isDistributed() (true for JDBCStore, false for the
in-memory ConcurrentStore default) rather than just documented as a
risk. A store that reports isDistributed() == false can still be
allowed explicitly via allowedObjectStores.
app.getClusterManager().getClusterMembers() — every live peer plus this
instance's own name, for a status endpoint or dashboard.
- Deliberately not built: per-job leader affinity (one elected instance
runs every clustered job in the app, not different jobs spread across
different instances), general peer-to-peer RPC (the relay only ever
carries a STOMP publish), and binary relay payloads (the envelope is
JSON text, matching STOMP's own transport limits here).
Middleware
- Body parsing:
boxExpressJSON(), boxExpressUrlencoded() — both cap
the body at 100KB by default (configurable), returning 413 over the
limit.
- Static files:
boxExpressStatic(dir, options) — directory-index
resolution, trailing-slash redirects, real-path containment against
symlink escapes.
- File uploads:
boxExpressUpload() (Multipart) — mirrors multer's
basic usage. Files land in req.files as arrays keyed by field name;
saved-to-disk files get generated UUID names (never the client's own
filename).
- Sessions:
boxExpressSession() — cookie-based, rolling expiry,
pluggable store (get/set/destroy). resave/saveUninitialized
options to skip unnecessary store writes. boxExpressCacheStore() backs
sessions with BoxLang's own cache service for durability across restarts
and sharing across a process cluster (including a real JDBC-backed table).
- Security headers:
boxExpressHelmet() — clickjacking, MIME-sniffing,
referrer, and cross-origin hardening headers, mirroring helmet's
defaults; every header individually overridable or disable-able.
Strict-Transport-Security/Content-Security-Policy are opt-in.
- CORS:
boxExpressCors() — origin reflection or allow-listing,
preflight handling, credentials, exposed/allowed headers, mirroring the
npm cors package.
- Rate limiting:
boxExpressRateLimit() — fixed-window limiting keyed by
req.ip (or a custom keyGenerator), draft-standard RateLimit-*
headers, 429 + Retry-After on breach.
- CSRF protection:
boxExpressCsrf() — session-based token strategy
mirroring csurf, req.csrfToken(), validated from a form field or
header on every unsafe method.
- Error-handling middleware detected the same way Express does (4-parameter
handlers), default
404/500 JSON responses, with real error messages
logged server-side and only echoed to the client when
app.set("env", "development").
Developer Experience
- Graceful shutdown: a JVM shutdown hook runs
close() on Ctrl-C/SIGTERM,
and a bad port bind fails with a friendly message instead of a raw stack
trace.
- Colored console output for the access log and restart notices, honoring
NO_COLOR.
- A full TestBox suite — router unit tests, real-HTTP integration tests, BIF
entry-point tests, Undertow adapter regression tests, cluster/leader-
election/relay integration tests, and process-lifecycle tests that spawn
a real subprocess to verify SIGTERM handling and
reloadOnChange.
Example Applications
Real, runnable apps built on BoxExpress, not just the snippets above:
- bxThreads — a public port of
DismalThreads, a Reddit-style forum app, built on BoxExpress instead of
ColdBox (no cbwire). Server-rendered HTML,
fetch()-driven actions, and
a STOMP-based realtime layer for comments, votes, and notifications —
the most complete example of app.ws()/boxExpressStomp() in a real
app rather than a test.
- bx-graphql-demo — a
runnable demo pairing BoxExpress with
bx-graphql, serving a small
seed dataset (users, posts, comments, shaped like JSONPlaceholder)
through a single
/graphql endpoint, plus a built-in browser query
console.
- examples/server.bxs
in the BoxExpress repo itself — a single-file tour of the whole API
surface (routing, middleware, views, sessions, uploads, SSE, and more),
meant to be read top to bottom or run directly with
boxlang examples/server.bxs.
What's Deliberately Not Built (Yet)
BoxExpress is upfront about its edges rather than papering over them:
- No cron-expression parsing in the scheduler (fixed-interval only).
- No persistence or missed-run catch-up for scheduled jobs.
- No per-job leader affinity across a cluster (one elected instance runs
every clustered job in the app) and no general peer-to-peer RPC between
cluster nodes — see Cluster Support above for what
clustering does cover.
- No binary WebSocket/STOMP bodies (a transport limit, not a broker choice).
- No multi-range (
bytes=0-10,20-30) or If-Range support for range
requests.
BoxExpress is available on ForgeBox as
boxlang-express. Full docs at
kisdigital.com/projects/boxlang-express/docs/getting-started,
architecture notes in
docs/ARCHITECTURE.md.
No comments yet — be the first.