Static File Middleware β
Serve static files like express.static()βwith caching, ETags, and path traversal protection.
Quick Start β
ts
import { bunway, serveStatic } from 'bunway';
const app = bunway();
app.use(serveStatic('public'));
app.listen(3000);Now files in ./public are served:
public/index.htmlβhttp://localhost:3000/public/css/style.cssβhttp://localhost:3000/css/style.csspublic/js/app.jsβhttp://localhost:3000/js/app.js
Mount Path β
Serve from a specific URL path:
ts
// Serve ./assets at /static
app.use('/static', serveStatic('assets'));
// assets/logo.png β http://localhost:3000/static/logo.pngOptions β
ts
interface StaticOptions {
index?: string | string[] | false; // Index files (default: 'index.html')
dotfiles?: 'allow' | 'deny' | 'ignore'; // Dotfile handling (default: 'ignore')
maxAge?: number; // Cache-Control max-age in ms (default: 0)
immutable?: boolean; // Add immutable to Cache-Control (default: false)
etag?: boolean; // Generate ETags (default: true)
lastModified?: boolean; // Set Last-Modified header (default: true)
fallthrough?: boolean; // Pass to next middleware if not found (default: true)
extensions?: string[]; // Try these extensions (default: [])
}Caching β
Cache Headers β
ts
// Cache for 1 day
app.use(serveStatic('public', {
maxAge: 86400000 // 1 day in ms
}));
// Cache forever (use with hashed filenames)
app.use('/assets', serveStatic('dist/assets', {
maxAge: 31536000000, // 1 year
immutable: true
}));ETags β
ETags are enabled by default. The server returns 304 Not Modified when the file hasn't changed:
ts
app.use(serveStatic('public', {
etag: true, // Default
lastModified: true // Default
}));Disable for performance:
ts
app.use(serveStatic('public', {
etag: false,
lastModified: false
}));Index Files β
Default β
By default, index.html is served for directory requests:
GET / β public/index.html
GET /about/ β public/about/index.htmlMultiple Index Files β
ts
app.use(serveStatic('public', {
index: ['index.html', 'index.htm', 'default.html']
}));Disable Index β
ts
app.use(serveStatic('public', {
index: false // Don't serve index files
}));File Extensions β
Automatically try extensions:
ts
app.use(serveStatic('public', {
extensions: ['html', 'htm']
}));
// GET /about β tries public/about, then public/about.html, then public/about.htmDotfiles β
Control access to dotfiles (.gitignore, .env, etc.):
ts
// Ignore (default) - return 404
app.use(serveStatic('public', { dotfiles: 'ignore' }));
// Deny - return 403
app.use(serveStatic('public', { dotfiles: 'deny' }));
// Allow - serve them
app.use(serveStatic('public', { dotfiles: 'allow' }));Fallthrough β
When a file isn't found:
ts
// Pass to next middleware (default)
app.use(serveStatic('public', { fallthrough: true }));
// Return 404 immediately
app.use(serveStatic('public', { fallthrough: false }));With fallthrough enabled:
ts
app.use(serveStatic('public'));
app.get('*', (req, res) => {
// Handle SPA routing
res.sendFile('public/index.html');
});Security β
Path Traversal Protection β
bunWay prevents path traversal attacks:
GET /../../../etc/passwd β 403 Forbidden
GET /..%2F..%2Fetc/passwd β 403 ForbiddenSymlink Protection β
Symlinks that point outside the root directory are blocked.
Recommendations β
ts
// Secure static file setup
app.use(helmet());
app.use(serveStatic('public', {
dotfiles: 'deny', // Block access to dotfiles
maxAge: 86400000, // Enable caching
etag: true // Enable conditional requests
}));Examples β
SPA with Client-Side Routing β
ts
const app = bunway();
// Serve static assets
app.use(serveStatic('dist', {
maxAge: 86400000
}));
// SPA fallback
app.get('*', (req, res) => {
res.sendFile('dist/index.html');
});Multiple Static Directories β
ts
// Serve uploads with no caching
app.use('/uploads', serveStatic('uploads', {
maxAge: 0,
dotfiles: 'deny'
}));
// Serve assets with long caching
app.use('/assets', serveStatic('public/assets', {
maxAge: 31536000000,
immutable: true
}));
// Serve other static files
app.use(serveStatic('public'));Development vs Production β
ts
const isDev = process.env.NODE_ENV !== 'production';
app.use(serveStatic('public', {
maxAge: isDev ? 0 : 86400000,
etag: !isDev
}));Migration from express.static() β
The API is nearly identical:
js
// Express
app.use(express.static('public'));
app.use('/assets', express.static('assets', { maxAge: '1d' }));
// bunWay
app.use(serveStatic('public'));
app.use('/assets', serveStatic('assets', { maxAge: 86400000 }));Note: bunWay uses milliseconds for maxAge instead of a string.