Skip to content

Express Static Files — Complete Guide to Serving CSS, JS, and Images

DodaTech Updated 2026-06-28 4 min read

In this tutorial, you will learn about Express Static Files. We cover key concepts, practical examples, and best practices to help you master this topic.

Express serves static files like CSS, JavaScript, and images using built-in express.static middleware, allowing browsers to access assets directly without route handlers.

What You'll Learn

By the end of this tutorial, you'll configure express.static for multiple directories, set virtual path prefixes, control Caching, optimize for production, and handle file not found scenarios.

Why Static Files Matter

Every web application needs CSS, client-side JavaScript, and images. express.static handles file serving efficiently with built-in caching, ETag support, and directory traversal prevention.

Real-World Use

A corporate website serves its CSS framework from /css, JavaScript bundles from /js, and product images from /images. express.static handles all three directories behind the same Express server.

Static Files Learning Path

flowchart LR
  A[Template Engines] --> B[Static Files]
  B --> C[Sessions]
  C --> D[Security]
  D --> E[REST API]
  A --> F{You Are Here}
  style F fill:#f90,color:#fff

Basic Static File Serving

import express from "express";
import path from "node:path";
const app = express();
app.use(express.static("public"));
// Files in ./public are served at the root:
// public/style.css → /style.css
// public/js/app.js → /js/app.js

Multiple Static Directories

app.use(express.static("public"));
app.use(express.static("uploads"));
// If same filename exists in both, public takes precedence

Virtual Path Prefix

app.use("/static", express.static("public"));
// public/style.css → /static/style.css
app.use("/assets", express.static("uploads"));
// uploads/images/logo.png → /assets/images/logo.png

Absolute Path

app.use("/static", express.static(path.join(process.cwd(), "public")));
// Always use absolute paths for reliable asset resolution

Caching Configuration

app.use(express.static("public", {
  maxAge: "1d",           // Cache for 1 day
  immutable: true,         // Never re-validate for hashed files
  etag: true,              // Enable ETag headers
  lastModified: true,      // Enable Last-Modified headers
  setHeaders: (res, path) => {
    if (path.endsWith(".css")) {
      res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
    }
  }
}));

Production Best Practices

const isProduction = process.env.NODE_ENV === "production";
app.use(express.static(path.join(process.cwd(), "dist"), {
  maxAge: isProduction ? "1y" : 0,
  immutable: isProduction,
  redirect: false,
  dotfiles: "ignore"
}));

Serving Build Artifacts

// For React/Vue/etc. built apps
app.use(express.static(path.join(process.cwd(), "client/build")));
app.get("*", (req, res) => {
  res.sendFile(path.join(process.cwd(), "client/build/index.html"));
});

Common Mistakes

1. Forgetting Absolute Paths in Production

Relative paths like "public" depend on the working directory. Use path.resolve or __dirname for reliability.

2. Serving Static Files Before Security Middleware

Sensitive files in the static directory are publicly accessible. Place security middleware before express.static.

3. No Cache Control in Production

Without cache headers, browsers re-download assets on every page load, wasting bandwidth and slowing page loads.

4. Serving node_modules Directly

Never serve node_modules publicly. It exposes source code, increases attack surface, and adds unnecessary requests.

5. Directory Listing Enabled

By default, express.static doesn't list directories. Don't accidentally enable it by misconfiguring options.

Practice Questions

1. What does express.static do?

It serves files from a directory on the filesystem, mapping file paths to URL paths. It handles headers, caching, and security automatically.

2. How do you serve files under a different URL path?

Use a mount path: app.use("/assets", express.static("public")). Files in public/ are served at /assets/filename.

3. How do you set cache headers for static files?

Pass maxAge option in milliseconds or string format: app.use(express.static("public", { maxAge: "7d" })).

4. Why use absolute paths for express.static?

The working directory may change. Absolute paths guarantee files are found regardless of where the Process is started.

5. Challenge: Configure express.static for a production app with versioned assets, long-term caching, and fallback to index.html for SPA routing.

app.use("/static", express.static(path.join(__dirname, "dist"), {
  maxAge: "1y",
  immutable: true
}));
app.get("*", (req, res) => {
  res.sendFile(path.join(__dirname, "dist/index.html"));
});

FAQ

What is the difference between express.static and a CDN?

express.static serves from your server. A CDN distributes files globally for faster delivery. Use both: CDN for production, express.static for development.

Does express.static protect against directory traversal?

Yes. express.static validates paths and prevents accessing files outside the specified directory.

How do I serve a single file that's not in the static directory?

Use res.sendFile() with an absolute path.

What is the ETag header?

ETag is a hash of the file content. Browsers send If-None-Match and get 304 Not Modified if the file hasn't changed, avoiding re-download.

Can I exclude certain files from being served?

Yes: app.use(express.static('public', { dotfiles: 'ignore', deny: ['*.json'] })).

Mini Project: Static Asset Server

Create a static file server with versioned assets and cache invalidation.

import express from "express";
import path from "node:path";
import crypto from "node:crypto";
import fs from "node:fs";
const app = express();
const hashFile = (filePath) => {
  const content = fs.readFileSync(filePath);
  return crypto.createHash("md5").update(content).digest("hex").slice(0, 8);
};
const files = fs.readdirSync("public/css");
const hashes = {};
files.forEach(f => { hashes[f] = hashFile(path.join("public/css", f)); });
app.set("view engine", "ejs");
app.get("/", (req, res) => {
  res.render("index", { styleHash: hashes["style.css"] });
});
app.use(express.static("public", { maxAge: "1y", immutable: true }));
app.listen(3000);

What's Next

Express Sessions Express Security REST API Express

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro