Static Files & Uploads
Serving a directory of static assets, sending individual files, and handling multipart uploads.
Serving a static directory
bxsapp.use( "/public", boxExpressStatic( expandPath( "./public" ) ) )
// GET /public/logo.png → serves ./public/logo.png
Mount it with no prefix to serve straight from the site root:
bxsapp.use( boxExpressStatic( expandPath( "./public" ) ) )
// GET /logo.png → serves ./public/logo.png
A request for a file that doesn't exist under the served directory falls through to next() rather than erroring — your other routes (or the default 404) still get a chance to handle it.
Conditional GET (ETag / 304)
Static file responses automatically set ETag and Last-Modified, and honor If-None-Match — a matching request gets a 304 Not Modified with no body, instead of re-sending the file. The same conditional-GET support applies to res.sendFile().
Cache-Control (options.maxAge)
Both accept options.maxAge (seconds) to also set Cache-Control: public, max-age=<n>, letting a browser skip revalidation entirely for that long instead of asking on every request. Off by default — no header at all unless asked for:
bxsapp.use( "/public", boxExpressStatic( expandPath( "./public" ), { maxAge: 86400 } ) ) // 1 day
app.get( "/report", ( req, res ) => {
res.sendFile( expandPath( "./reports/latest.pdf" ), { maxAge: 3600 } ) // 1 hour
} )
HEAD requests
HEAD /public/logo.png returns the same headers a GET would — Content-Type, Content-Length, ETag, Last-Modified — with no response body. See Routing for how HEAD works across the framework generally.
Range requests (partial content)
Both static file serving and res.sendFile()/download() honor a Range request header and respond 206 Partial Content with just the requested slice — what makes video/audio scrubbing and resumable downloads work, since the client doesn't have to (re-)download the whole file to seek or resume. Every file response sets Accept-Ranges: bytes, even a full 200, so a client knows it can send a Range request on a later one:
bashcurl -H "Range: bytes=0-1023" localhost:3000/public/video.mp4
# -> 206, Content-Range: bytes 0-1023/<total>, body is just those 1024 bytes
A range starting beyond the file's size gets a 416 Range Not Satisfiable with Content-Range: bytes */<total> and no body. Two things are out of scope, the trade-off most minimal static-file servers make: a request naming more than one range falls back to a full 200 response instead of a multipart/byteranges reply, and If-Range (a conditional range against a validator) isn't supported — a Range request is always attempted regardless of freshness.
Sending a single file from a route
bxsapp.get( "/report", ( req, res ) => {
res.sendFile( expandPath( "./reports/latest.pdf" ) )
} )
app.get( "/report/download", ( req, res ) => {
res.download( expandPath( "./reports/latest.pdf" ), "report.pdf" )
} )
res.download() is sendFile() with Content-Disposition: attachment forced, defaulting the downloaded filename to the source file's own name if you don't pass one. A caller-supplied filename is sanitized — quote and control characters are stripped so it can't inject extra Content-Disposition parameters.
When building a file path from user input (e.g. a route param), pass options.root to sandbox it — a resolved path that escapes root is rejected rather than served:
bxsapp.get( "/files/:name", ( req, res ) => {
res.sendFile( req.params.name, { root: expandPath( "./uploads" ) } )
} )
Handling uploads
bxsapp.post(
"/upload",
boxExpressUpload( { dest: expandPath( "./uploads" ) } ),
( req, res ) => {
res.json( { fields: req.body, files: req.files } )
}
)
boxExpressUpload() parses a multipart/form-data request: non-file fields land in req.body same as any other body parser. req.files is a struct keyed by field name, each value an array of file structs — a field can carry more than one file — with shape { fieldName, filename, contentType, size, buffer, path }. path is only present when dest was given; the client-supplied filename is never used to build it, sidestepping path-traversal/collision entirely.
bxsapp.get( "/uploaded-avatar", ( req, res ) => {
res.json( {
filename: req.files.avatar[ 1 ].filename,
size: req.files.avatar[ 1 ].size,
savedAt: req.files.avatar[ 1 ].path
} )
} )