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