Express.js Static Files Serving

Static files are files that never change based on user input — HTML pages, CSS stylesheets, JavaScript files, images, fonts, and PDFs. Express has a built-in middleware called express.static() that serves these files directly to the browser without any custom route handling. This topic covers how to set up static file serving and control how it works.

What "Static" Means

Think of a vending machine. Every customer gets the same product from the same slot — the machine does not prepare a custom meal for each person. Static files work the same way. The server hands the same file to every request for that file. A CSS stylesheet looks the same regardless of who visits, so Express serves it directly from disk without running any JavaScript logic.

Browser requests: /css/style.css
                       │
                       ▼
          Express checks the public/ folder
                       │
         ┌─────────────▼──────────────┐
         │  public/                   │
         │  ├── css/                  │
         │  │   └── style.css ← FOUND │
         │  ├── images/               │
         │  └── js/                   │
         └─────────────┬──────────────┘
                       │
                       ▼
         File sent directly to browser
         (no route handler needed)

Setting Up Static File Serving

Create a folder named public in your project root. Place your CSS, images, and frontend JavaScript files inside it. Then add one line to your Express app:

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

app.use(express.static('public'));

app.listen(3000);

Express now automatically serves any file inside the public folder. No custom routes needed.

Project Structure with Static Files

my-express-app/
│
├── public/                 ← Static files live here
│   ├── index.html
│   ├── css/
│   │   └── style.css
│   ├── js/
│   │   └── main.js
│   └── images/
│       ├── logo.png
│       └── banner.jpg
│
├── app.js                  ← Express server
└── package.json

With this structure, these URLs serve the corresponding files automatically:

URL                            → File Served
─────────────────────────────────────────────────────
http://localhost:3000/         → public/index.html
http://localhost:3000/css/style.css  → public/css/style.css
http://localhost:3000/js/main.js     → public/js/main.js
http://localhost:3000/images/logo.png → public/images/logo.png

Why index.html Loads at the Root URL

When someone visits http://localhost:3000/, Express looks for a file named index.html inside the static folder automatically. This follows the same convention web servers like Apache and Nginx use. You do not need to define a GET / route to serve the homepage — express.static() handles it.

Using an Absolute Path

The string 'public' is a relative path. If you run your app from a different directory, Node.js might look for the wrong folder. Use path.join() with __dirname to create an absolute path that always points to the right place:

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

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

app.listen(3000);

__dirname is a Node.js variable that holds the absolute path to the folder containing your current file. path.join() combines it with 'public' safely regardless of the operating system.

Virtual Path Prefix

By default, files serve from the root URL. You can add a virtual prefix so files appear under a specific path segment without moving them:

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

Now the same files serve under the /assets prefix:

Before prefix:                 After prefix:
/css/style.css          →      /assets/css/style.css
/images/logo.png        →      /assets/images/logo.png
/js/main.js             →      /assets/js/main.js

The physical files stay in the public folder. Only the URL changes. This is useful when you want to distinguish static asset URLs from API endpoint URLs.

Serving Multiple Static Directories

Call express.static() multiple times to serve files from more than one folder:

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

Express searches the folders in order. If a file named photo.jpg exists in both public and uploads, Express serves the one from public because it appears first.

Caching with maxAge

Browsers cache static files so they don't download the same file on every visit. Express lets you set the cache duration (how long the browser keeps the file before checking for updates):

app.use(express.static('public', {
  maxAge: '1d'   // Cache files for 1 day
}));

Cache duration options:

┌──────────────────────────────────────────────────────┐
│               maxAge Examples                        │
├──────────────────┬───────────────────────────────────┤
│ '1d'             │ 1 day                             │
│ '7d'             │ 7 days                            │
│ '30d'            │ 30 days                           │
│ '1y'             │ 1 year (use for fingerprinted     │
│                  │  files like main.abc123.js)       │
│ 0                │ No cache (always re-fetch)        │
└──────────────────┴───────────────────────────────────┘

The dotfiles Option

Files that start with a dot (like .htaccess or .env) are hidden files. By default, Express ignores them for security. Change this behavior with the dotfiles option:

app.use(express.static('public', {
  dotfiles: 'ignore'   // Default: ignores dotfiles
  // dotfiles: 'deny'  // Returns 403 Forbidden
  // dotfiles: 'allow' // Serves them (use with caution)
}));

Setting a Custom Index File

By default, index.html serves as the directory index. Change it to a different filename:

app.use(express.static('public', {
  index: 'home.html'  // Serves home.html instead of index.html
}));

Disable directory indexing entirely by setting it to false, which is useful when you handle the root route with a custom handler:

app.use(express.static('public', {
  index: false
}));

app.get('/', (req, res) => {
  res.send('Custom home page via route handler');
});

Static Files and Route Conflict

When you define both a static folder and a route for the same path, the order of app.use() calls determines which wins. Static files registered first take priority over routes for the same path:

// Static middleware registered first
app.use(express.static('public'));

// This route only runs if /info.html does NOT exist in public/
app.get('/info.html', (req, res) => {
  res.send('This only runs when no static file matches');
});

Summary

Use express.static() to serve HTML, CSS, JavaScript, images, and other files directly from a folder. Create a public folder in your project, call app.use(express.static('public')), and Express handles all file delivery automatically. Use path.join(__dirname, 'public') for a reliable absolute path. Add a URL prefix with app.use('/assets', express.static(...)) to namespace your static assets. Control browser caching with the maxAge option and protect sensitive files by keeping them outside the static folder entirely.

Leave a Comment

Your email address will not be published. Required fields are marked *