Serving Static Files

1. Using express.static() Middleware

FormEffect
express.static("public")Serve at root
app.use("/static", express.static("public"))Mount under prefix
express.static("public", opts)Pass serve-static options

2. Setting Static Directory

Example: Absolute path

import path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));

app.use(express.static(path.join(__dirname, "public")));
Warning: Always use absolute paths — relative paths resolve from the CWD where Node was launched, breaking PM2 / systemd setups.

3. Serving Multiple Static Directories

Example: Order matters (first found wins)

app.use(express.static("dist"));      // Built assets
app.use(express.static("public"));    // Untouched static

4. Setting Virtual Path Prefix

Example: Mount under /assets

app.use("/assets", express.static("public"));
// File public/logo.png → GET /assets/logo.png
BenefitDetail
DecouplingURL doesn't expose folder name
Versioning/assets/v2/...
CDN aliasingMatch CDN path

5. Configuring Cache Headers

Asset TypemaxAgeimmutable
Hashed bundles (app.[hash].js)1ytrue
Images30dfalse
index.html0false (no-cache)

Example: Long-cache hashed assets

app.use("/static", express.static("dist", {
  maxAge: "1y",
  immutable: true,
  setHeaders: (res, filePath) => {
    if (filePath.endsWith(".html")) res.setHeader("Cache-Control", "no-cache");
  }
}));

6. Setting Index Files

SettingEffect
index: "index.html"Default
index: ["index.html", "index.htm"]Try in order
index: falseDisable index serving

7. Enabling Directory Listing

Example: serve-index for browsing

import serveIndex from "serve-index";
app.use("/files", express.static("uploads"), serveIndex("uploads", { icons: true }));
Warning: Never enable directory listing in production for sensitive folders — exposes filenames.

8. Setting ETag Headers

OptionDefaultPurpose
etagtrueGenerate weak ETag from inode/size/mtime
lastModifiedtrueSend Last-Modified header
Client request 1: GET /logo.png
Server response:  200 OK + ETag: "abc123"

Client request 2: GET /logo.png + If-None-Match: "abc123"
Server response:  304 Not Modified  ← no body, saves bandwidth
        

9. Handling Fallback Files

Example: SPA fallback to index.html

app.use(express.static("dist"));

// Catch-all: serve SPA for unknown paths (Express 5)
app.get("/{*splat}", (req, res) => {
  res.sendFile(path.join(__dirname, "dist/index.html"));
});
Use CasePattern
SPACatch-all → index.html
Multi-tenantPer-tenant static dir

10. Serving Hidden Files

dotfilesBehavior
"ignore" (default)404 + skip dotfiles
"deny"403 Forbidden
"allow"Serve normally

Example: Allow .well-known for ACME

app.use(express.static("public", { dotfiles: "allow" }));
// Or scope it to ACME challenge only
app.use("/.well-known", express.static(".well-known", { dotfiles: "allow" }));