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 )
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.
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. 🙂
No comments yet — be the first.