express.static is the built-in middleware that serves files from a folder. app.use(express.static(path.join(__dirname, 'public'))) makes every file in public available at the same path in the URL: public/style.css answers /style.css, and public/index.html answers /. Express sets the Content-Type from the file extension, and when no file matches it passes the request on to your routes instead of answering 404.
The answer to GET /style.css is the file's text, with Content-Type: text/css; charset=utf-8 in the Headers tab. Send GET / to get index.html, then GET /api/time: there is no such file, so the route answers.
Why the folder path needs __dirname
express.static('public') resolves 'public' against the folder you started Node from, not the folder server.js is in. It works when you run node server.js inside the project and breaks when a process manager or a script starts the app from somewhere else:
// project/server.js uses express.static('public')
// cd project && node server.js -> serves project/public
// node project/server.js -> looks for ./public, every file is 404
path.join(__dirname, 'public') is the folder next to the file that contains the line, wherever Node was started. In an ES module, use path.join(import.meta.dirname, 'public') (Node 20.11 and later). The path module page covers __dirname and path.join in detail.
Serve files under a prefix
Pass a mount path first to serve the folder under a URL prefix. The prefix is not part of the file's path on disk:
GET /assets/app.js returns console.log("hi"); as text/javascript, and GET /assets/img/dot.svg reaches into the subfolder. Send GET /app.js without the prefix: it is a 404, because the files only exist under /assets. Use a prefix to keep file URLs from colliding with your API routes.
Cache headers with maxAge
By default Express sends Cache-Control: public, max-age=0, so browsers check with the server on every use (and get a fast 304 Not Modified when the file has not changed, thanks to the ETag header). For files whose names change on every build, such as app.3f9a1c.js, tell browsers to keep them:
Open the Headers tab: Cache-Control is now public, max-age=604800, seven days in seconds. index: false turns off serving index.html for folder URLs. Other useful options are extensions: ['html'], which lets /about find about.html, and dotfiles, which is 'ignore' by default so files like .env are never served.
Single page apps: fall back to index.html
A React or Vue app built for client-side routing needs every unknown path to return index.html, so a reload on /settings does not 404. Put the fallback after express.static and your API routes:
app.use(express.static(path.join(__dirname, 'dist')));
app.use('/api', apiRouter);
// Express 5 wildcard syntax
app.get('/*splat', (req, res) => {
res.sendFile(path.join(__dirname, 'dist', 'index.html'));
});
Common mistakes
Including the folder name in the URL. With app.use(express.static('public')) the URL is /style.css, not /public/style.css. If you want /public in the URL, mount it: app.use('/public', express.static(...)).
Linking files with relative paths in HTML. <link href="style.css"> on the page /blog/post asks for /blog/style.css. Start the path with a slash: /style.css.
Putting secrets in the static folder. Everything in it is public. Keep config files, keys and source code out of it.
Frequently Asked Questions
Can I use express.static more than once?
Yes. Register one per folder; Express looks in them in the order you added them and serves the first file it finds. Give each one its own mount path when the folders could hold files with the same name.
Should Express serve static files in production?
For small apps it is fine. For heavy traffic, put a CDN or a reverse proxy such as Nginx in front, which serves files faster and caches them closer to users, and keep Express for the API.
How do I send one specific file from a route?
Use res.sendFile(path.join(__dirname, 'files', 'report.pdf')). The path must be absolute (or you pass a root option). Use res.download() instead when the browser should save the file rather than display it.