BoxLang Express Enhancements
BoxLang Express went from basic routing to a full-featured framework across 10 releases (0.1.1→0.1.14): sessions, CSRF, CORS, rate limiting, security headers, uploads, and a SQL-backed session store. Every fix got verified live before being called done, then locked in as one of 149 passing tests. Along the way, express-test grew from a chat demo into a live documentation site, catching stale docs and gotchas that reading alone would've missed.
BoxExpress, 0.1.1 → 0.1.14
I started BoxExpress as an Express.js-style web framework for BoxLang — routing, middleware, mountable routers, .bxm view rendering, all on top of a standalone HttpServer, none of it borrowed from a servlet container. Since 0.1.1 it's gone from "routes basically work" to something with sessions, CSRF, CORS, rate limiting, security headers, file uploads with range requests, graceful shutdown, and now a SQL-backed session store. Here's what actually changed, and what express-test — which quietly turned into the project's real documentation site along the way — picked up at the same time.
The security pass (0.1.3 → 0.1.4)
The earliest real bugs were security bugs, not feature gaps. res.download() interpolated a caller-supplied filename straight into Content-Disposition unescaped — an attacker-controlled filename could break out of the quoted token and inject extra header parameters. And the default 500 handler echoed the raw exception message back to unauthenticated clients, which I confirmed leaks absolute server filesystem paths off nothing more than a malformed JSON body hitting the opt-in body parser. Both fixed before anything else got built on top of them: res.download() strips quotes/control characters from the filename now, and the default 500 body is a generic "Internal Server Error" unless the app explicitly opts in with app.set("env", "development").
Same window added app.param(), app.route(), and conditional GET (ETag/Last-Modified, answering a matching If-None-Match with an empty 304 instead of re-sending the file) — the last of the "make it behave like Express" gaps before I moved on to things Express doesn't have to think about, like BoxLang's own CLI runtime not keeping a process alive on its own. listen() used to return immediately, which meant every consumer needed its own while(true) { sleep(1000) } tacked on after — a footgun I hit twice in the same session against the same test app before fixing it. It blocks by default now, Node-style.
Sessions, then everything that needed sessions (0.1.6 → 0.1.14)
boxExpressSession() landed early — opaque ID in the cookie, data server-side, rolling expiry, and a pluggable store option from day one even though the only implementation for a long time was the in-memory default. That one design choice is why CSRF protection later dropped in cleanly: boxExpressCsrf() mirrors csurf's session-based token strategy, throws immediately if req.session doesn't exist rather than silently no-op'ing (a misconfigured CSRF check should be loud, not a hole nobody notices until production).
The rest of 0.1.14's middleware batch follows the same pattern — port the npm package's common-case defaults, verify live against a throwaway app before writing a single test: boxExpressHelmet() (security headers, HSTS/CSP opt-in rather than on-by-default since BoxExpress's HttpServer never terminates TLS itself and a generic CSP breaks real apps blindly applied), boxExpressCors() (preflight answered directly since nothing else would handle OPTIONS on an arbitrary route), and boxExpressRateLimit() (fixed-window, same in-memory-store trade-off and amortized sweep-on-Nth-write cleanup as Session).
Most recently: boxExpressCacheStore(), a Session store backed by BoxLang's own cache() service instead of a bare ConcurrentHashMap. Point the named cache at objectStore: "JDBCStore" and sessions live in a real SQL table — durable across restarts, shared across a cluster hitting the same database.
Correctness bugs, the unglamorous kind
Not every fix was a feature. app.use(mw1, mw2, mw3) — completely ordinary Express usage — used to crash: only the first handler ever registered, and the second got silently mistaken for a mount path. app.get() routes didn't automatically answer HEAD, which was also a real bug in static file serving specifically — it accepted HEAD requests fine, then sent the full file body anyway, a spec violation. And reloadOnChange had a nasty one: a failed restart (bad process relaunch, a transient OS limit) tore down the old server before confirming the new one actually started, so a bad restart didn't just fail to reload, it took the whole process down silently. Fixed by reordering — launch the replacement first, only close the old one once the new one is actually up, log and keep running if the launch itself throws.
Each of these got verified by reproducing the actual failure first — a throwaway app, a curl request, a confirmed crash or wrong response — before I called it fixed, and then turned into a permanent regression test. The suite's at 149 tests now, all passing, none of them added after the fact without first watching them fail against the bug they cover.
express-test: from chat demo to documentation site
express-test didn't start as BoxExpress's docs. It began as a small chat app (sanitizing input with bx-esapi before publishing it — that project's own security lesson). I rebuilt it into what it is now: a real, running BoxExpress app that doubles as the documentation site — Getting Started, Configuration, Routing, Middleware, Request & Response, Views, Sessions, Static Files & Uploads, Error Handling, Process Lifecycle — every page live and clickable against an actual server, not a static doc dump.
That mattered immediately. Cross-checking the main README against express-test's own pages caught four things that had gone stale or were never written down at all: req.rawExchange() missing from the Request API list, res.dump() missing despite existing since 0.1.11, a reloadOnChange description describing the old, buggy restart order instead of the fixed one, and an install gotcha (a global module install silently wins over a project-local one, even though the config lists local first) that had been verified on express-test's own Getting Started page but never made it back to the main README.
Every new BoxExpress feature since has shipped with an express-test update in the same breath — not a changelog bullet, an actual live example: rate limiting demoed against the real /upload route's 10-per-5-minutes limit, CSRF demoed against that same form's real hidden _csrf field (edit it in dev tools, submit, watch the 403), a from-scratch Google OAuth middleware example built to show how to write custom middleware rather than assume BoxExpress ships OAuth support, and — where a feature genuinely can't be demoed live on a same-origin, no-external-datasource site — an honest "No live demo here" callout instead of a fake one. The CORS section says so outright: this site has no genuine cross-origin request to demonstrate against. The CacheStore section says the same about its SQL-backed store: verified against a real database while building it, just not wired into this site's own sessions.
Two smaller, non-obvious findings landed on the Configuration page purely from testing rather than reading: boxlang.json in a project directory is not auto-loaded (only ~/.boxlang/config/boxlang.json, machine-wide, unless you pass --bx-config explicitly) — confirmed by dumping the runtime's actual resolved config from inside a project with its own file present. And .env, unlike boxlang.json, is auto-loaded from the current working directory with no flag needed, with a real OS environment variable always winning over the .env file's value for the same name — also confirmed directly, not assumed, by running the same script with and without a .env present and again with a conflicting shell variable set.
Where it stands
10 releases, 149 tests, and — going back through the commit log to write this — a pattern I'm glad held up under review: almost nothing here got fixed or documented from reading the BoxLang docs and trusting them. Bugs got reproduced live before being called bugs; fixes got verified live before being called fixed; anything that ended up in the README or on express-test's docs pages got there because I ran it and watched what actually happened.