All Articles

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. 🙂

github.com/robertz/boxlang-express