Serving Static Files
Static files are assets that don’t change - HTML, CSS, JavaScript, images, fonts, PDFs, etc. Express makes serving these files simple with the built-in express.static() middleware.
Basic Static File Serving
Serve files from a directory:
const express = require('express');
const app = express();
// Serve files from 'public' directory
app.use(express.static('public'));
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
Project structure:
project/
├── server.js
└── public/
├── index.html
├── about.html
├── css/
│ └── style.css
├── js/
│ └── app.js
└── images/
└── logo.png
Access files:
http://localhost:3000/index.htmlhttp://localhost:3000/css/style.csshttp://localhost:3000/js/app.jshttp://localhost:3000/images/logo.png
Serving from Custom Path
Mount static files at a specific URL path:
// Serve files from 'public' at '/static' route
app.use('/static', express.static('public'));
// Now access files with /static prefix:
// http://localhost:3000/static/style.css
// http://localhost:3000/static/app.js
This is useful for:
- Namespacing assets
- Separating public from API routes
- Better organization
Multiple Static Directories
Serve files from multiple folders:
const express = require('express');
const app = express();
// Serve from multiple directories
app.use(express.static('public'));
app.use(express.static('uploads'));
app.use(express.static('downloads'));
// Express checks directories in order
// First match wins
Directory priority:
public/index.html → Served
uploads/index.html → Ignored (public/index.html found first)
Absolute Paths
Use absolute paths for reliability:
const express = require('express');
const path = require('path');
const app = express();
// Better: use absolute path
const publicPath = path.join(__dirname, 'public');
app.use(express.static(publicPath));
// For different locations
const uploadsPath = path.join(__dirname, '..', 'uploads');
app.use('/uploads', express.static(uploadsPath));
Index File Handling
Express automatically serves index.html for directory requests:
app.use(express.static('public'));
// public/index.html exists
// http://localhost:3000/ → serves public/index.html
// http://localhost:3000/index.html → same file
// public/about/index.html exists
// http://localhost:3000/about/ → serves public/about/index.html
Static Options
Configure static middleware behavior:
const options = {
// Enable directory indexing (default: false)
index: 'index.html',
// Set max age for browser cache (in milliseconds)
maxAge: '1d', // 1 day
// Enable/disable ETags (default: true)
etag: true,
// Custom 404 behavior
fallthrough: true, // Continue to next middleware if file not found
// Set response headers
setHeaders: (res, filePath) => {
if (filePath.endsWith('.html')) {
res.set('Cache-Control', 'no-cache');
}
}
};
app.use(express.static('public', options));
Caching
Control browser caching:
// No caching (good for development)
app.use(express.static('public', {
maxAge: 0
}));
// Cache for 1 day (good for production)
app.use(express.static('public', {
maxAge: '1d'
}));
// Cache for 1 year (for versioned assets)
app.use('/assets', express.static('public', {
maxAge: '1y'
}));
Serving HTML Pages
Simple HTML Server
const express = require('express');
const path = require('path');
const app = express();
app.use(express.static('public'));
// Fallback for SPA (Single Page Applications)
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
app.listen(3000);
Serving Specific Pages
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
app.get('/about', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'about.html'));
});
app.get('/contact', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'contact.html'));
});
Combining Static Files with API
Typical structure: serve frontend files + API:
const express = require('express');
const path = require('path');
const app = express();
// Parse JSON for API routes
app.use(express.json());
// API routes
app.get('/api/users', (req, res) => {
res.json([
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
]);
});
app.post('/api/users', (req, res) => {
res.status(201).json({ message: 'User created' });
});
// Serve static files (after API routes)
app.use(express.static('public'));
// SPA fallback - must be last
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
app.listen(3000);
Important: API routes should come before static middleware to avoid conflicts.
File Downloads
Force file downloads instead of viewing:
const express = require('express');
const path = require('path');
const app = express();
// Force download
app.get('/download/report', (req, res) => {
const file = path.join(__dirname, 'files', 'report.pdf');
res.download(file);
});
// Download with custom filename
app.get('/download/latest', (req, res) => {
const file = path.join(__dirname, 'files', 'data-2024-01.csv');
res.download(file, 'data.csv'); // User sees 'data.csv'
});
// Download with callback
app.get('/download/secure', (req, res) => {
const file = path.join(__dirname, 'files', 'confidential.pdf');
res.download(file, (err) => {
if (err) {
console.error('Download failed:', err);
// Send error response if headers not sent yet
if (!res.headersSent) {
res.status(500).send('Download failed');
}
} else {
console.log('File downloaded successfully');
}
});
});
Security Considerations
Path Traversal Prevention
Never trust user input for file paths:
// ❌ DANGEROUS - User can access any file
app.get('/files/:filename', (req, res) => {
const file = path.join(__dirname, 'uploads', req.params.filename);
res.sendFile(file); // User could request '../../../etc/passwd'
});
// ✅ SAFE - Validate and sanitize
app.get('/files/:filename', (req, res) => {
const filename = path.basename(req.params.filename); // Remove path components
const file = path.join(__dirname, 'uploads', filename);
// Ensure file is within uploads directory
if (!file.startsWith(path.join(__dirname, 'uploads'))) {
return res.status(403).send('Access denied');
}
res.sendFile(file);
});
Hide Source Files
Don’t expose sensitive files:
// Serve public files
app.use(express.static('public'));
// But NOT:
// app.use(express.static('.')); // Exposes server.js, package.json, .env!
Keep structure like:
project/
├── server.js # Not accessible
├── .env # Not accessible
├── package.json # Not accessible
└── public/ # Publicly accessible
└── ...
Complete Example
const express = require('express');
const path = require('path');
const app = express();
// Parse JSON bodies
app.use(express.json());
// Logger
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
});
// API Routes (before static files)
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
];
app.get('/api/users', (req, res) => {
res.json(users);
});
app.post('/api/users', (req, res) => {
const newUser = {
id: users.length + 1,
...req.body
};
users.push(newUser);
res.status(201).json(newUser);
});
// Static files
const publicPath = path.join(__dirname, 'public');
app.use(express.static(publicPath, {
maxAge: '1h',
setHeaders: (res, filePath) => {
// No cache for HTML, cache everything else
if (filePath.endsWith('.html')) {
res.set('Cache-Control', 'no-cache');
}
}
}));
// File downloads
app.get('/download/sample', (req, res) => {
res.download(path.join(__dirname, 'files', 'sample.pdf'));
});
// SPA fallback - serves index.html for all unmatched routes
app.get('*', (req, res) => {
res.sendFile(path.join(publicPath, 'index.html'));
});
// Start server
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
public/index.html:
<!DOCTYPE html>
<html>
<head>
<title>My App</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<h1>Welcome</h1>
<div id="app"></div>
<script src="/js/app.js"></script>
</body>
</html>
public/js/app.js:
// Fetch data from API
fetch('/api/users')
.then(res => res.json())
.then(users => {
const app = document.getElementById('app');
app.innerHTML = users.map(u => `<p>${u.name}</p>`).join('');
});
Best Practices
✅ DO:
- Use absolute paths with
path.join(__dirname, ...) - Put API routes before static middleware
- Set appropriate cache headers for production
- Validate user-provided filenames
- Keep sensitive files outside public directory
❌ DON’T:
- Serve your entire project directory
- Trust user input for file paths
- Expose .env, package.json, or source files
- Forget to set cache headers
- Use relative paths
Summary
| Task | Code |
|---|---|
| Serve directory | app.use(express.static('public')) |
| With path prefix | app.use('/assets', express.static('public')) |
| Set cache | express.static('public', { maxAge: '1d' }) |
| Send file | res.sendFile(path.join(__dirname, 'file.html')) |
| Force download | res.download(filePath) |
| Absolute path | path.join(__dirname, 'public') |
Next Article: Form Handling