Static Files#
Serve static files from a directory using app.static(). This is useful for serving images, CSS, JavaScript bundles, or any other static assets.
Basic Usage#
// Serve files from ./public at /public/*
app.static("/public", "./public");
// Serve files from ./assets at /files/*
app.static("/files", "./assets");Parameters#
| Parameter | Type | Description |
|---|---|---|
| routePath | string | URL prefix (e.g., "/public") |
| directory | string | Filesystem path (relative to process.cwd() or absolute) |
| options | StaticOptions? | maxAge, cacheControl, etag, native |
Security#
Directory traversal attacks are automatically prevented. Requests that attempt to escape the base directory (e.g., /files/../../etc/passwd) will receive a 403 Forbidden response.
Caching#
Static file responses include caching headers:
Cache-Control: public, max-age=3600(1 hour)- ETag support - Server sends
ETagheader on304 Not Modifiedresponses. Clients should sendIf-None-Matchto leverage caching.
404 Handling#
If the requested file does not exist, the server returns a 404 response with { error: 'File Not Found' }.
Example#
// Project structure:
// ./public/
// ├── index.html
// ├── styles.css
// └── images/
// └── logo.png
app.static("/public", "./public");
// GET /public/index.html → serves index.html
// GET /public/styles.css → serves styles.css
// GET /public/images/logo.png → serves logo.png
// GET /public/missing.txt → 404 Not FoundNative Directory Serving (native: true)#
Pass { native: true } to hand the directory to Bun's built-in static file router: MIME type, ETag, Range, and index.html resolution happen inside Bun — requests never enter JavaScript.
app.static("/public", "./public", { native: true });Bun owns the response, so there are three wire differences from the default JS handler:
- No
Cache-Controlheader — set caching at your CDN or proxy instead. - Unknown files get an empty-body
404(not the JSON{ error: 'File Not Found' }). ETag/If-None-Matchare handled by Bun.
Promotion is automatic only when it cannot change the wire: with X-Powered-By enabled, global middleware, or request logging active, the JS handler keeps serving (byte-identical to native: false).
Serving Individual Files (file())#
For a file chosen per-request (download link, avatar, report), return file(path) from a handler. It is lazy - no disk I/O at registration; the framework converts it to a response at response time so Range requests are honored (video/audio seeking):
import { file } from "@buntok/core";
app.get("/video", () => file("public/kyuukurarin.mp4"));
app.get("/report", () =>
file("exports/report.pdf", {
type: "application/pdf", // optional MIME override (auto-detected from extension)
headers: { "Cache-Control": "no-store" },
}),
);Responses include Accept-Ranges: bytes and Content-Range; a valid Range header returns 206, and an out-of-range request returns 416. file() has no fallback when the file is missing - use serveFileOrFallback() from @buntok/core when you need one.
Next Steps#
- Routing - Learn about route groups and other routing features
- Middleware - Add middleware to static file routes