Serving Static Files
Learn how to serve CSS, JavaScript, images, and other static assets with Express
What are Static Files?
Static files are files that don’t change based on the request - they’re the same for every user. Common examples:
- CSS stylesheets (
style.css) - Client-side JavaScript (
app.js) - Images (
logo.png,photo.jpg) - Fonts (
Roboto.woff2) - PDF documents (
manual.pdf) - HTML files (
about.html)
express.static()
Express provides built-in middleware express.static() to serve static files:
const express = require('express');
const app = express();
app.use(express.static('public'));
app.listen(3000);
With this setup, files in the public/ directory are served at the root path:
Project Structure:
project/
├── public/
│ ├── css/
│ │ └── style.css
│ ├── js/
│ │ └── app.js
│ └── images/
│ └── logo.png
├── index.js
└── package.json
URLs:
http://localhost:3000/css/style.css
http://localhost:3000/js/app.js
http://localhost:3000/images/logo.png
Virtual Path Prefix
You can add a mount path so static files look like they’re served from a subdirectory:
app.use('/static', express.static('public'));
Now files are served with a /static prefix:
http://localhost:3000/static/css/style.css
http://localhost:3000/static/images/logo.png
Multiple Static Directories
You can serve files from multiple directories. Express searches them in order:
app.use(express.static('public'));
app.use(express.static('uploads'));
app.use(express.static('assets'));
If the same filename exists in multiple directories, the first match wins.
public/logo.png → http://localhost:3000/logo.png (wins if exists here)
uploads/logo.png → http://localhost:3000/logo.png (only served if not in public)
Absolute Paths
Always use absolute paths to avoid issues when running the app from different directories:
const path = require('path');
// ❌ Relative path - depends on where you run the command
app.use(express.static('public'));
// ✅ Absolute path - always works
app.use(express.static(path.join(__dirname, 'public')));
__dirname gives the directory of the current file, making the path reliable.
Full Working Example
const express = require('express');
const path = require('path');
const app = express();
const port = 3000;
// Serve static files from 'public' directory
app.use(express.static(path.join(__dirname, 'public')));
// API route alongside static files
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', timestamp: Date.now() });
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Express Static Files</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<div class="container">
<h1>Welcome to Express Static Files</h1>
<img src="/images/logo.png" alt="Logo" width="200">
<p id="message"></p>
</div>
<script src="/js/app.js"></script>
</body>
</html>
public/css/style.css
body {
font-family: Arial, sans-serif;
background: #f0f0f0;
margin: 0;
padding: 40px;
}
.container {
background: white;
padding: 40px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
max-width: 600px;
margin: 0 auto;
text-align: center;
}
h1 {
color: #333;
}
img {
border-radius: 8px;
margin: 20px 0;
}
public/js/app.js
document.getElementById('message').textContent =
'This page is served by Express static middleware!';
.env or Config-Driven Static Path
For production, use environment variables:
const publicDir = process.env.PUBLIC_DIR || 'public';
app.use(express.static(path.join(__dirname, publicDir)));
Sending Specific Files with sendFile
When you need to send a specific file from a route handler, use res.sendFile():
app.get('/download', (req, res) => {
const filePath = path.join(__dirname, 'files', 'document.pdf');
res.sendFile(filePath);
});
You can also use it with the root option:
app.get('/download/:file', (req, res) => {
const options = {
root: path.join(__dirname, 'files'),
dotfiles: 'deny',
headers: {
'x-timestamp': Date.now(),
'x-sent': true
}
};
res.sendFile(req.params.file, options, (err) => {
if (err) {
console.error(err);
res.status(404).send('File not found');
}
});
});
express.static() Options
The express.static() function accepts an options object:
const options = {
dotfiles: 'ignore', // How to handle dotfiles: 'allow' | 'deny' | 'ignore'
etag: true, // Enable ETag generation
extensions: ['html', 'htm'], // Fallback to extensions if file not found
fallthrough: true, // Pass to next middleware if file not found
immutable: false, // Set immutable directive in Cache-Control
index: 'index.html', // Directory index file (false to disable)
lastModified: true, // Set Last-Modified header
maxAge: '1d', // Cache-Control max-age
redirect: true, // Redirect to trailing '/' when path is a directory
setHeaders: (res, path, stat) => {
// Custom headers based on file type
if (path.endsWith('.wasm')) {
res.set('Content-Type', 'application/wasm');
}
}
};
app.use(express.static('public', options));
Caching Headers
Set maxAge to control browser caching:
// Cache for 1 day
app.use(express.static('public', { maxAge: '1d' }));
// Cache for 1 year (for fingerprinted assets)
app.use(express.static('public', { maxAge: '1y', immutable: true }));
// No caching (for development)
app.use(express.static('public', { maxAge: 0 }));
Security Considerations
1. Avoid exposing sensitive files
Files outside the static directory are NOT accessible:
✅ /public/secret.txt → http://localhost:3000/secret.txt (served)
❌ /secret.txt → http://localhost:3000/../secret.txt (blocked)
❌ /node_modules/express/index.js → http://localhost:3000/../node_modules/... (blocked)
Express prevents directory traversal attacks by default.
2. Don’t serve node_modules
Never do this:
// ❌ Security risk - exposes all your dependencies
app.use(express.static('node_modules'));
3. Use dotfiles option
app.use(express.static('public', {
dotfiles: 'deny' // Prevent serving .env, .gitignore, etc.
}));
4. Production best practices
const express = require('express');
const path = require('path');
const app = express();
const isProduction = process.env.NODE_ENV === 'production';
app.use(express.static(path.join(__dirname, 'public'), {
dotfiles: 'deny',
maxAge: isProduction ? '30d' : 0,
etag: isProduction,
lastModified: isProduction,
setHeaders: (res, filePath) => {
if (filePath.endsWith('.html')) {
res.set('Cache-Control', 'no-cache');
}
}
}));
// Fallback to index.html for SPA routing (React, Vue, etc.)
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
Real-World Project Structure
my-app/
├── public/ # Static assets (served to clients)
│ ├── index.html # Main HTML page
│ ├── css/
│ │ ├── main.css
│ │ └── vendor.css
│ ├── js/
│ │ ├── app.js
│ │ └── vendor.js
│ ├── images/
│ │ ├── logo.svg
│ │ └── hero.jpg
│ ├── fonts/
│ │ └── inter.woff2
│ └── favicon.ico
├── views/ # Server-side templates (if using a template engine)
│ └── index.ejs
├── routes/ # Route modules
│ └── api.js
├── middleware/ # Custom middleware
│ └── auth.js
├── node_modules/
├── index.js # Entry point
├── package.json
└── .gitignore
Key Takeaways
express.static('public')serves files from thepublic/directory- Use a virtual path prefix (
/static) to namespace static files - Always use absolute paths with
path.join(__dirname, ...) - Configure caching with the
maxAgeoption - Express prevents directory traversal attacks automatically
- Use
res.sendFile()for one-off file deliveries - For SPAs, add a catch-all route after static middleware
Keep learning
Related tutorials from across the library.
Introduction to Express.js
Learn what Express.js is, its philosophy, and why it's the most popular Node.js web framework
Web Development / Backend / Express / Basics
Installing Express & Project Setup
Step-by-step guide to setting up a Node.js project with Express from scratch
Web Development / Backend / Express / Basics
First Express App (app.get, app.listen)
Build your first Express server with route handling and HTTP methods
Web Development / Backend / Express / Basics