NukeBase

Serving Static Files

Serve files from a local directory using app.serveStatic. This is typically the very next thing you'll register after addDomain — it powers your frontend, assets, and any other files the browser needs. Registers both GET and HEAD handlers automatically — HEAD answers with the headers and the file's Content-Length and no body, and an empty file is answered immediately with an empty body:

Basic static file serving
// Serve everything in ../public at the root
nukebase.app.serveStatic("/*", path.join(__dirname, "../public"));

// Serve a private directory (no auth callback = open access)
nukebase.app.serveStatic("/assets/*", path.join(__dirname, "assets"));
Parameter order: The serveStatic auth callback receives (res, req) — res first, then req. This matches the underlying uWebSockets convention and is the opposite of Express.

Signature

app.serveStatic(routePattern, rootDir, auth?)

  • routePattern - URL pattern with trailing /* (e.g., "/*", "/admin/*"). The matched portion before /* is treated as the mount point and stripped before resolving against rootDir.
  • rootDir - Absolute path to the directory to serve from. Use path.join(__dirname, "...").
  • auth (optional) - Async callback returning a boolean. If provided, runs before any file is served. Return true to allow, false to respond with 401.

Auth Callback

The auth callback receives (res, req) — same convention as postWithBody:

Static files with authentication
// Only logged-in users can access /private/*
nukebase.app.serveStatic("/private/*", path.join(__dirname, "../private"),
  async (res, req) => {
    return Boolean(req.uid);
  }
);

// Admins only
nukebase.app.serveStatic("/admin/*", path.join(__dirname, "../admin"),
  async (res, req) => {
    return req.claims?.role === "admin";
  }
);

The req object passed to the auth callback contains the auth fields populated by checkAuth (uid, username, email, claims, cookies, urlParams, referer, userAgent, ip, url) plus host and method. Identity fields are present only when the user is authenticated. Returning a falsy value sends a 401 response automatically.

The callback is a hook, not just a gate. It runs on every request before a byte is served and receives res, so it is the natural place for cross-cutting work — request logging, page-view analytics, setting a cookie. Do the work, then return true to serve the file normally. Keep it cheap and never let it throw: it sits on the path of every asset your site serves.

Behavior Details

  • Index files: Requests ending in / automatically serve index.html from that directory.
  • Path traversal protection: Any URL that resolves outside rootDir (e.g., via ../) returns 403 Forbidden.
  • Directories: A URL resolving to a directory without a trailing slash gets a 301 Moved Permanently to the same URL with one added, so relative links inside the served page resolve correctly.
  • Missing files: Return 404 Not Found.
  • Streaming: Files are streamed in 16KB chunks rather than buffered into memory — safe for large files.
  • Content-Type: Set automatically from the file extension via the mime package; falls back to application/octet-stream for unknown types.
  • Validators: Every file carries a weak ETag (from size and mtime) and a Last-Modified header. A request with a matching If-None-Match (or, without one, If-Modified-Since) is answered 304 Not Modified with no body, so checking whether a file changed costs a round trip and no bytes.
  • Cache-Control: no-cache for most files, which means "always revalidate" — browsers never serve a stale copy on their own, and a push shows up on the next load. Files with image/* content types keep public, max-age=31536000 (1 year), which is why an icon is renamed rather than replaced. Files served by a mount whose auth callback read who the caller is (see below) get private, no-store.

Files gated on a person are never cached. If the auth callback reads any identity field — req.uid, req.username, req.email, req.token, req.claims or req.cookies — the response is marked private, no-store, so neither the browser's HTTP cache nor the service worker's page cache keeps it, and it can never be shown to the next person on a shared device. The template's () => true reads nothing, so an ordinary site stays cacheable. Reading req.ip or req.url (for logging, say) does not make a file private.

↑