Serving Static Files
1. Using express.static() Middleware
| Form | Effect |
|---|---|
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
| Benefit | Detail |
|---|---|
| Decoupling | URL doesn't expose folder name |
| Versioning | /assets/v2/... |
| CDN aliasing | Match CDN path |
5. Configuring Cache Headers
| Asset Type | maxAge | immutable |
|---|---|---|
Hashed bundles (app.[hash].js) | 1y | true |
| Images | 30d | false |
index.html | 0 | false (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
| Setting | Effect |
|---|---|
index: "index.html" | Default |
index: ["index.html", "index.htm"] | Try in order |
index: false | Disable 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
| Option | Default | Purpose |
|---|---|---|
etag | true | Generate weak ETag from inode/size/mtime |
lastModified | true | Send 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 Case | Pattern |
|---|---|
| SPA | Catch-all → index.html |
| Multi-tenant | Per-tenant static dir |
10. Serving Hidden Files
| dotfiles | Behavior |
|---|---|
"ignore" (default) | 404 + skip dotfiles |
"deny" | 403 Forbidden |
"allow" | Serve normally |