Skip to content

Serving Static Files in Express

Static files, images, stylesheets, client-side JavaScript bundles, don't need a custom route handler, Express can serve an entire folder of them with one line. This lesson covers express.static() and its options.

Serving a Public Folder

express.static(root) is built-in middleware that serves files directly from a given directory. Any request matching a file inside that directory is served automatically, with correct Content-Type headers, no route definitions needed.

A common convention is a public/ folder at the project root containing images, CSS, and client-side JS.

const express = require('express');
const path = require('path');
const app = express();

app.use(express.static(path.join(__dirname, 'public')));

// public/style.css is now available at /style.css
// public/images/logo.png is now available at /images/logo.png

path.join(__dirname, 'public') builds an absolute path, safer than a relative string that depends on the current working directory.

Mounting Static Files at a Prefix

app.use('/static', express.static(path.join(__dirname, 'public')));

// public/style.css is now available at /static/style.css
  • Mounting at a prefix keeps static asset URLs visually distinct from your API routes.
  • You can register express.static() multiple times, for multiple directories or prefixes.
  • express.static() automatically serves index.html for a directory request by default.
  • Files not found simply fall through to the next middleware/route, they don't automatically 404 the whole app.

Static Files Cheatsheet

Common express.static() configurations.

Use Case Code
Serve at root app.use(express.static('public'))
Serve under a prefix app.use('/static', express.static('public'))
Multiple directories app.use(express.static('public')); app.use(express.static('uploads'));
Set cache headers express.static('public', { maxAge: '1d' })
Disable directory index.html express.static('public', { index: false })

Caching Static Assets

Static assets rarely change once deployed (especially when filenames include content hashes from a build tool), so setting cache headers lets browsers avoid re-downloading them on every visit.

app.use(express.static(path.join(__dirname, 'public'), {
  maxAge: '7d',
  etag: true,
}));

Serving a Frontend Build

A common pattern for full-stack apps serves a built single-page app (React, Vue, etc.) directly from Express, falling back to index.html for client-side routing on any unmatched path.

app.use(express.static(path.join(__dirname, 'client/dist')));

app.get('*', (req, res) => {
  res.sendFile(path.join(__dirname, 'client/dist/index.html'));
});

This catch-all should be registered after your API routes so it doesn't intercept them.

Common Mistakes

  • Using a relative path for the static directory instead of path.join(__dirname, ...), breaking depending on where the process is started from.
  • Registering the SPA catch-all route before your API routes, silently swallowing every API request.
  • Serving sensitive files (like .env or server source) from a directory accidentally exposed as static.
  • Not setting any cache headers, forcing browsers to re-download unchanged assets on every visit.

Key Takeaways

  • express.static(root) serves an entire directory of files with one line, no manual routes needed.
  • Always use an absolute path built with path.join(__dirname, ...).
  • Cache headers (maxAge, etag) reduce repeat downloads for unchanged assets.
  • Serving a frontend build's index.html as a catch-all must come after your API routes.

Pro Tip

Never point express.static() at your project root or any directory containing source code, .env, or configuration files, only ever point it at a dedicated, purpose-built public assets folder.