Introducing BoxExpress: Express.js ergonomics for BoxLang
BoxExpress brings Express.js-style HTTP endpoints to BoxLang using the JDK's built-in HTTP server. It supports routing, middleware, sub-routers, static files, and view rendering—without requiring a servlet container.
Every time I needed a quick HTTP endpoint, I missed how fast this is in Node:
const app = require('express')()
app.get('/', (req, res) => res.send('Hello World'))
app.listen(3000)
BoxLang has no equivalent. It does have the JVM underneath it, which means
com.sun.net.httpserver.HttpServer — a standalone HTTP server that's been in
the JDK since Java 6, no servlet container needed. So I built the fifteen-second
version on top of it. BoxExpress, on ForgeBox now.
app = boxExpress()
app.get( "/", ( req, res ) => {
res.send( "Hello World" )
} )
app.listen( 3000 )
// this is needed to keep the process alive; explained below
while ( true ) {
sleep( 1000 )
}
Routing, middleware, mountable sub-routers, view rendering — a plain BoxLang process. No CommandBox server, no WAR, nothing to deploy but a script.
Install
box install boxlang-express
app = boxExpress()
boxExpress() is a global function, registered as soon as BoxLang discovers
the module. No require() equivalent needed.
Routes and params
If you've used Express, you already know this:
app.get( "/users/:id", ( req, res ) => {
res.json( { id: req.params.id } )
} )
app.get( "/search", ( req, res ) => {
res.json( { query: req.query } )
} )
Middleware
Same (req, res, next) shape. Same next(err) to jump to error-handling
middleware. Same trick Express uses to tell the two apart: an error handler
has four params instead of three.
app.use( boxExpressJSON() )
app.post( "/echo", ( req, res ) => {
res.json( { youSent: req.body } )
} )
app.use( ( err, req, res, next ) => {
res.status( 500 ).json( { error: true, message: err.message } )
} )
boxExpressJSON() caps request bodies at 100KB by default. "Lightweight
framework" shouldn't also mean "one curl -d with a big enough payload kills
the process."
Mounting a router
apiRouter = new boxexpress.models.Router()
apiRouter.get( "/ping", ( req, res ) => {
res.json( { pong: true } )
} )
app.use( "/api", apiRouter )
GET /api/ping hits it. Inside the router, req.path is scoped to /ping —
mount prefix stripped and restored around it, same as Express.
Views — including Handlebars
BoxLang has its own template format, and
res.render() supports it natively:
app.set( "views", expandPath( "./views" ) )
app.get( "/greet/:name", ( req, res ) => {
res.render( "greeting", { name: req.params.name } )
} )
Not everyone wants <bx:output>#data.name#</bx:output> though, so
render() also handles .hbs via a bundled Handlebars engine. No separate
install:
app.get( "/greet-hbs/:name", ( req, res ) => {
res.render( "greeting.hbs", { data: { name: req.params.name } } )
} )
<h1>Hello, {{data.name}}!</h1>
Pick per-view by extension, or set a default with app.set("view engine", "hbs").
Static files
app.use( "/public", boxExpressStatic( expandPath( "./public" ) ) )
Paths get resolved against the real, symlink-resolved directory before
serving — a symlink dropped inside public/ can't be used to walk out of it.
The one gotcha
app.listen() doesn't block — neither does Express's. In Node the process
stays alive because the event loop still has work (timers, open sockets).
BoxLang's CLI runtime has no equivalent, so if listen() is the last line,
the process exits right after printing "listening on 3000." Hit this myself
the first time I ran a standalone app outside this project. Fix is one line:
app.listen( 3000 )
while ( true ) {
sleep( 1000 )
}
Why this exists
A friend recently asked if BoxLang could listen for and respond to simple HTTP requests from a .bxs script. Since that functionality didn't exist at the time, I quickly put this together.
Now it does. 🙂