javascript-today

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.html
  • http://localhost:3000/css/style.css
  • http://localhost:3000/js/app.js
  • http://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')