Most web applications need to deliver files that never change per request: stylesheets, client-side JavaScript, images, fonts, and downloadable documents. Express handles this with a single built-in middleware, express.static. This lesson shows you how to set it up, how paths are resolved, and the options that matter in production.
Create a folder named public in your project and register the middleware:
const express = require('express');
const path = require('path');
const app = express();
app.use(express.static(path.join(__dirname, 'public')));
app.listen(3000, () => console.log('Static server on port 3000'));Now any file inside public is served automatically. If public/styles.css exists, a request to /styles.css returns it with the correct Content-Type header. No route definitions required.
Passing a bare 'public' works only when you start the server from the project folder. If you launch it from elsewhere, Node resolves the relative path against your current working directory and files mysteriously 404. Building the path from __dirname pins it to the file location, which is robust no matter where the process starts.
A typical project looks like this:
project/
index.js
public/
index.html
css/style.css
js/app.js
images/logo.png
With the middleware in place, /index.html, /css/style.css, and /images/logo.png all resolve. Even better, a request to / automatically serves public/index.html because index.html is the default index file.
Inside your HTML, reference assets with root-relative paths. The public folder itself never appears in URLs:
<link rel="stylesheet" href="/css/style.css">
<script src="/js/app.js" defer></script>
<img src="/images/logo.png" alt="Logo">A very common beginner mistake is writing /public/css/style.css. That returns 404 because express.static mounts the contents of the folder at the root, not the folder name.
Sometimes you want assets under a URL prefix, for example /static. Pass the prefix as the first argument to app.use:
app.use('/static', express.static(path.join(__dirname, 'public')));Now public/css/style.css is served at /static/css/style.css. Prefixes are useful when your API routes and asset URLs might otherwise collide.
You can register several static folders. Express checks them in order and serves the first file it finds:
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.static(path.join(__dirname, 'uploads')));If both folders contain report.pdf, the one in public wins because it was registered first, consistent with the general middleware ordering rule.
By default files are re-sent on every request. In production, tell browsers to cache them with the maxAge option:
app.use(express.static('public', {
maxAge: '7d',
etag: true,
}));This sets a Cache-Control header so repeat visitors load assets from their local cache, cutting bandwidth and load times dramatically. Pair long cache lifetimes with versioned filenames like app.v2.js so updates still reach users.
Next lesson: Handling POST Requests and JSON Bodies, where clients finally start sending data to the server.