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#

typescript
// Serve files from ./public at /public/*
app.static("/public", "./public");

// Serve files from ./assets at /files/*
app.static("/files", "./assets");

Parameters#

ParameterTypeDescription
routePathstringURL prefix (e.g., "/public")
directorystringFilesystem path (relative to process.cwd() or absolute)
optionsStaticOptions?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 ETag header on 304 Not Modified responses. Clients should send If-None-Match to leverage caching.

404 Handling#

If the requested file does not exist, the server returns a 404 response with { error: 'File Not Found' }.

Example#

typescript
// 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 Found

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

typescript
app.static("/public", "./public", { native: true });

Bun owns the response, so there are three wire differences from the default JS handler:

  • No Cache-Control header — 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-Match are 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):

typescript
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