๐Ÿ”ท Core

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.

$ npx skills add ortus-boxlang/skills/boxlang-developer/web-development
$ coldbox ai skills install ortus-boxlang/skills/boxlang-developer/web-development
๐Ÿ”— https://skills.boxlang.io/skills/raw/ortus-boxlang/skills/boxlang-developer~web-development

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:

ResponseCallbackYou 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 false stops the stream and closes the connection immediately (so the server stops generating). null, 0, true or no return value keep streaming. Stopping is not an error: the status code is unchanged and result.streamCompleted is false.
  • 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 a 408 through onError. Buffered requests keep the normal timeout meaning.
  • Errors: a non 2xx status never reaches the chunk callback. The body is read and reported through onError as a message like HTTP 401: {"error":"bad key"}, and result.statusCode and result.fileContent hold the status and body.
  • No accumulation: binary mode does not fill result.fileContent. Use result.chunkCount and result.totalBytes. file and path are not applied, so write the bytes yourself.
  • Precedence: if both onBinaryChunk and onChunk are set, onBinaryChunk is used.
  • Gotchas: bytes is a Java byte[], so use arrayLen( bytes ) for its size. In bx:http script syntax the bx:httpparam lines 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 servicesModern REST/JSON APIs
WSDL-defined contractsLightweight, fast communication
WS-Security requiredJSON preferred
Complex type mappingSimple HTTP calls

References