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 )

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

github.com/robertz/boxlang-express

No comments yet — be the first.