boxlang-web-development
Use this skill when building BoxLang web applications: Application.bx lifecycle, request/response handling, sessions, forms, REST APIs, HTTP clients (including binary and SSE response streaming), routing, CSRF protection, Server-Sent Events, or configuring CommandBox/MiniServer.
BoxLang Web Development
BoxLang is the AI-native software productivity platform for building, modernizing and running applications, with developers and AI agents working together.
Overview
BoxLang web applications revolve around Application.bx, which acts as the
application lifecycle controller. The runtime handles HTTP requests through a
well-defined lifecycle, with full support for REST, sessions, CSRF, SSE, and
both MiniServer (dev) and CommandBox (production) deployment.
For a deep dive into multi-app isolation, descriptor discovery, and advanced
Application.bx patterns, see the
application-descriptor skill.
Application.bx
Application.bx lives at the web root and configures the entire application:
// Application.bx
class {
// Application settings
this.name = "MyApp"
this.sessionManagement = true
this.sessionTimeout = createTimeSpan( 0, 2, 0, 0 ) // 2 hours
this.applicationTimeout = createTimeSpan( 1, 0, 0, 0 ) // 1 day
this.setClientCookies = true
// Java classpath additions
this.javaSettings = {
loadPaths: [ "lib/" ],
loadColdFusionClassPath: false,
reloadOnChange: false
}
// Datasource defaults
this.datasource = "mainDB"
// --- Lifecycle Events ---
// Fires once when the application first starts
boolean function onApplicationStart() {
application.version = "1.0.0"
application.config = loadConfig()
return true
}
// Fires before every request
boolean function onRequestStart( required string targetPage ) {
// Auth checks, request logging, CORS headers
if ( !isAuthenticated() && !isPublicRoute( arguments.targetPage ) ) {
location( url="/login", addToken=false )
}
return true
}
// Fires after every request
void function onRequestEnd( required string targetPage ) {
// Cleanup, audit logging
}
// Called when request target is a .bx file (optional โ allows custom routing)
void function onRequest( required string targetPage ) {
include arguments.targetPage
}
// Fires when a new session starts
boolean function onSessionStart() {
session.cart = []
session.userId = ""
session.isLoggedIn = false
return true
}
// Fires when a session ends
void function onSessionEnd( required struct sessionScope, required struct appScope ) {
auditService.recordLogout( arguments.sessionScope.userId )
}
// Global error handler
boolean function onError( required any exception, required string eventName ) {
errorService.log( arguments.exception )
include "views/error.bxm"
return true
}
// Missing template handler
boolean function onMissingTemplate( required string targetPage ) {
location( url="/404" )
return true
}
}
Request Scope and CGI Variables
// URL variables (?key=value)
var page = url.page ?: 1
var query = url.q ?: ""
// Form variables (POST body)
var username = form.username ?: ""
var password = form.password ?: ""
// CGI scope
var userAgent = cgi.http_user_agent
var requestMethod = cgi.request_method // GET, POST, PUT, DELETE
var remoteAddr = cgi.remote_addr
var serverName = cgi.server_name
// Request scope (per-request shared storage)
request.startTime = getTickCount()
request.userId = session.userId
Handling Forms
// Process a form submission
if ( cgi.request_method == "POST" ) {
// Validate
if ( !len( trim( form.email ) ) ) {
request.errors.append( "Email is required" )
}
if ( !request.errors.len() ) {
userService.create({
email : form.email,
username : form.username,
password : hashPassword( form.password )
})
location( url="/dashboard", addToken=false )
}
}
REST API Patterns
// REST endpoint using onRequest routing in Application.bx
void function onRequest( required string targetPage ) {
var method = cgi.request_method
var path = cgi.path_info
var router = new Router()
router.get( "/api/users", "handlers.UsersHandler", "index" )
router.post( "/api/users", "handlers.UsersHandler", "create" )
router.get( "/api/users/:id", "handlers.UsersHandler", "show" )
router.put( "/api/users/:id", "handlers.UsersHandler", "update" )
router.delete( "/api/users/:id", "handlers.UsersHandler", "delete" )
router.dispatch( method, path )
}
// Handler: handlers/UsersHandler.bx
class {
function index() {
var users = userService.findAll()
renderJSON( users )
}
function show( required numeric id ) {
var user = userService.findById( arguments.id )
if ( isNull( user ) ) {
httpStatus( 404 )
renderJSON({ error: "Not found" })
return
}
renderJSON( user )
}
function create() {
var data = jsonDeserialize( getHTTPRequestData().content )
var user = userService.create( data )
httpStatus( 201 )
renderJSON( user )
}
}
// Helper functions
function renderJSON( required any data ) {
bx:header name="Content-Type" value="application/json";
writeOutput( jsonSerialize( arguments.data ) )
abort
}
function httpStatus( required numeric code ) {
bx:header statusCode=arguments.code;
}
HTTP Client
// Simple GET
var response = httpGet( "https://api.example.com/data" )
var data = jsonDeserialize( response.fileContent )
// Full bx:http request
bx:http url="https://api.example.com/users" method="POST" result="apiResponse" {
bx:httpparam type="header" name="Authorization" value="Bearer #token#"
bx:httpparam type="header" name="Content-Type" value="application/json"
bx:httpparam type="body" value=jsonSerialize({ name: "Ada", email: "[email protected]" })
}
var response = jsonDeserialize( apiResponse.fileContent )
// HTTP with query string
bx:http url="https://api.example.com/search" method="GET" result="searcharesult" {
bx:httpparam type="url" name="q" value="boxlang"
bx:httpparam type="url" name="limit" value="10"
}
// File download
bx:http url="https://example.com/report.pdf" method="GET" result="pdfResponse"
getAsBinary="yes"
fileWrite( expandPath( "./downloads/report.pdf" ), pdfResponse.fileContent )
Streaming Responses (BoxLang 1.19.0+)
Pass a callback to http() or bx:http to process a response as it arrives instead of buffering it. There are two kinds of streaming callback, and choosing the right one matters:
| Response | Callback | You receive |
|---|---|---|
| Binary (audio, images, downloads) | onBinaryChunk( bytes, info ) | Raw byte[] per network read |
| Server-Sent Events | .sse( true ) + onChunk( event, lastEventId, httpResult, httpClient, response ) | A struct { data, event, id, retry } per event |
| Plain text lines (legacy) | onChunk( chunkNumber, line, totalBytes, ... ) | One line at a time, decoded as text |
Never use line-based onChunk for binary data. It decodes the body as text and splits on newlines, which corrupts the bytes. Use onBinaryChunk.
// Binary streaming: restream audio from a provider, forwarding bytes as they arrive
result = http( "https://api.example.com/v1/speech" )
.post()
.header( "Authorization", "Bearer #apiKey#" )
.jsonBody( '{"text":"Hello from BoxLang","format":"mp3"}' )
.timeout( 30 )
.onBinaryChunk( ( bytes, info ) => {
// info: chunkNumber (1-based), totalBytes (so far), result, httpClient,
// and headers on the FIRST chunk only
if ( info.chunkNumber == 1 ) {
println( "Content-Type: " & info.headers[ "Content-Type" ] )
}
socket.send( bytes )
// An explicit false stops streaming and closes the connection
return !socket.isClosed()
} )
.onError( ( error, httpResult ) => {
println( "Stream failed: #error.message#" )
} )
.send()
println( "Received #result.totalBytes# bytes in #result.chunkCount# chunks, completed=#result.streamCompleted#" )
// Save a large download to disk without holding it in memory
out = createObject( "java", "java.io.FileOutputStream" ).init( expandPath( "./downloads/audio.mp3" ) )
try {
http( "https://example.com/audio.mp3" )
.onBinaryChunk( ( bytes, info ) => out.write( bytes ) )
.send()
} finally {
out.close()
}
// bx:http component form (script syntax)
bx:http url="https://example.com/audio.mp3" method="GET" result="audio"
onBinaryChunk=function( bytes, info ) {
return info.totalBytes < 8192 // stop after ~8KB
} {}
println( audio.streamCompleted ) // false
// SSE: an explicit false from onChunk stops the stream (for example on a [DONE] sentinel)
http( "https://api.example.com/events" )
.sse( true )
.timeout( 30 )
.onChunk( ( event ) => {
if ( event.data == "[DONE]" ) {
return false
}
println( event.data )
} )
.send()
Rules for onBinaryChunk and SSE streams:
- Stop on false: only an explicit boolean
falsestops the stream and closes the connection immediately (so the server stops generating).null,0,trueor no return value keep streaming. Stopping is not an error: the status code is unchanged andresult.streamCompletedisfalse. - Idle timeout: while streaming,
timeout(seconds) is the longest wait for the response headers or between received bytes. It does not cap the total duration, so long streams keep working. A stalled stream is aborted and reported as a408throughonError. Buffered requests keep the normal timeout meaning. - Errors: a non 2xx status never reaches the chunk callback. The body is read and reported through
onErroras a message likeHTTP 401: {"error":"bad key"}, andresult.statusCodeandresult.fileContenthold the status and body. - No accumulation: binary mode does not fill
result.fileContent. Useresult.chunkCountandresult.totalBytes.fileandpathare not applied, so write the bytes yourself. - Precedence: if both
onBinaryChunkandonChunkare set,onBinaryChunkis used. - Gotchas:
bytesis a Javabyte[], so usearrayLen( bytes )for its size. Inbx:httpscript syntax thebx:httpparamlines inside the body need a trailing;. Requires BoxLang 1.19.0 or newer.
Session Management
// Set session values
session.userId = user.getId()
session.username = user.getUsername()
session.isLoggedIn = true
session.cart = []
// Check session
if ( session.isLoggedIn ?: false ) {
// Authenticated
}
// Invalidate session on logout
sessionInvalidate()
location( url="/login", addToken=false )
// Extend session programmatically
sessionRotate()
CSRF Protection
// Generate CSRF token (stores in session)
var token = csrfGenerateToken( "loginForm" )
// In your form (bxm template):
// <input type="hidden" name="csrfToken" value="#csrfGenerateToken('loginForm')#">
// Validate on POST
if ( !csrfVerifyToken( form.csrfToken, "loginForm" ) ) {
httpStatus( 403 )
writeOutput( "Invalid CSRF token" )
abort
}
Server-Sent Events (SSE)
// SSE endpoint: sse/updates.bxs
bx:header name="Content-Type" value="text/event-stream";
bx:header name="Cache-Control" value="no-cache";
bx:header name="X-Accel-Buffering" value="no";
var i = 0
while ( i < 100 ) {
var data = getLatestUpdates()
writeOutput( "data: #jsonSerialize(data)##char(10)##char(10)#" )
flush
sleep( 1000 )
i++
}
Output and Rendering
// Include a template
include "views/header.bxm"
include "views/user/profile.bxm"
// Save output to variable
var rendered = ""
savecontent variable="rendered" {
include "views/email/welcome.bxm"
}
// Set response headers
cfheader( name="X-Custom-Header", value="myValue" )
cfcontent( type="application/pdf" )
// Redirect
location( url="/dashboard", addToken=false )
location( url="/login", statusCode=302 )
MiniServer Configuration (Development)
miniserver.json in the project root:
{
"host": "localhost",
"port": 8080,
"webroot": "./www",
"debug": true,
"warmUpURLs": ["/"],
"rewrites": {
"enable": true,
"config": "urlrewrite.xml"
}
}
Start MiniServer:
boxlang-miniserver
# or via CommandBox
box server start
CommandBox Server Configuration (Production)
server.json:
{
"name": "MyApp",
"web": {
"http": { "port": 8080 },
"https": { "port": 8443, "enable": true },
"webroot": "www"
},
"app": {
"cfengine": "boxlang",
"javaVersion": "openjdk21_jdk"
},
"jvm": {
"heapSize": "512m",
"maxHeapSize": "1024m",
"args": "-Duser.timezone=UTC"
},
"env": {
"DB_HOST": "localhost",
"DB_NAME": "myapp"
}
}
SOAP Web Services
BoxLang 1.8.0+ provides the soap() BIF for consuming SOAP 1.1/1.2 web services.
It automatically parses WSDL documents and converts SOAP XML responses to BoxLang
native types.
Basic SOAP Client
// Create client from WSDL URL
var ws = soap( "http://example.com/service.wsdl" )
// Invoke an operation with named parameters
var result = ws.invoke( "getCustomer", { customerId: 12345 } )
writeOutput( "Name: #result.customerName#" )
Client with Authentication and Timeout
var ws = soap( "http://example.com/service.wsdl" )
.withBasicAuth( "apiUser", "secret" )
.timeout( 30 )
var customer = ws.invoke( "getCustomer", { customerId: 42 } )
Custom Headers
var ws = soap( "http://secure.example.com/service.wsdl" )
.header( "X-API-Key", "abc123" )
.header( "X-Tenant-ID", "tenant001" )
.timeout( 45 )
var result = ws.invoke( "getData" )
Service Inspection (Discover Operations)
var ws = soap( "http://example.com/service.wsdl" )
// List all available operations
var operations = ws.getOperations()
operations.each( ( op ) -> writeOutput( op & "<br>" ) )
// Get input parameter details for an operation
var info = ws.getOperationInfo( "createOrder" )
info.inputParameters.each( ( param ) -> {
writeOutput( "#param.name# (#param.type#)<br>" )
})
Error Handling (SOAP Faults โ BoxLang Exceptions)
try {
var ws = soap( "http://example.com/service.wsdl" )
var result = ws.invoke( "processOrder", { orderId: orderId } )
} catch ( "soap.Fault" e ) {
writeOutput( "SOAP fault: #e.message#" )
} catch ( "soap.ConnectionError" e ) {
writeOutput( "Connection failed: #e.message#" )
} catch ( any e ) {
writeOutput( "Unexpected error: #e.message#" )
}
When to use soap() vs bx:http:
Use soap() | Use bx:http / httpGet() |
|---|---|
| Enterprise/legacy SOAP services | Modern REST/JSON APIs |
| WSDL-defined contracts | Lightweight, fast communication |
| WS-Security required | JSON preferred |
| Complex type mapping | Simple HTTP calls |