Skip to main content

Command Palette

Search for a command to run...

Storing Uploaded Files and Serving Them in Express

Understanding where files are stored and how to serve them with Express

Updated
6 min read
J

Turning chai into code and ideas into full-stack applications. Sharing lessons from my development journey, one commit at a time.


Where Uploaded Files Are Stored

When users upload files to your Node.js application, you must decide where to store them:

1. Local Storage (Server Filesystem)

Files saved directly on the server's disk.

const multer = require('multer');
const path = require('path');

const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, 'uploads/'); // Store in uploads folder
  },
  filename: (req, file, cb) => {
    cb(null, Date.now() + path.extname(file.originalname));
  }
});

const upload = multer({ storage });

File location:

project/
  ├── uploads/
  │   ├── 1640000000000.jpg
  │   ├── 1640000001000.pdf
  │   └── 1640000002000.png
  ├── server.js
  └── package.json

2. External Storage (Cloud Services)

Files uploaded to third-party services like:

  • AWS S3

  • Google Cloud Storage

  • Azure Blob Storage

  • Cloudinary (for images)

Why use external storage:

  • Scalability (no server disk limitations)

  • Reliability (built-in redundancy)

  • CDN integration (faster global access)

  • Separation of concerns (storage separate from application)


Local Storage vs External Storage

Aspect Local Storage External Storage
Setup Simple, no extra services Requires cloud account setup
Cost Free (uses server disk) Pay per GB stored and transferred
Scalability Limited by disk space Virtually unlimited
Performance Fast for single server CDN provides global speed
Backup Manual backup needed Automatic redundancy
Server Migration Must move files manually Files remain accessible
Use Case Small apps, prototypes Production apps, large scale

Serving Static Files in Express

Static files (images, PDFs, videos) need to be accessible via URLs. Express provides middleware to serve files from specific directories.

Basic Static File Serving

const express = require('express');
const app = express();

// Serve files from 'uploads' directory
app.use('/files', express.static('uploads'));

app.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
});

How it works:

  • Files in uploads/ folder are served at /files/* URL path

  • uploads/1640000000000.jpghttp://localhost:3000/files/1640000000000.jpg

Multiple Static Directories

// Serve different file types from different folders
app.use('/images', express.static('uploads/images'));
app.use('/documents', express.static('uploads/documents'));
app.use('/public', express.static('public'));

Folder structure:

project/
  ├── uploads/
  │   ├── images/
  │   │   └── photo.jpg     → http://localhost:3000/images/photo.jpg
  │   └── documents/
  │       └── report.pdf    → http://localhost:3000/documents/report.pdf
  └── public/
      └── style.css         → http://localhost:3000/public/style.css

Accessing Uploaded Files via URL

Complete Upload and Serve Example

const express = require('express');
const multer = require('multer');
const path = require('path');

const app = express();

// Configure storage
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, 'uploads/');
  },
  filename: (req, file, cb) => {
    const uniqueName = Date.now() + '-' + Math.round(Math.random() * 1E9);
    cb(null, uniqueName + path.extname(file.originalname));
  }
});

const upload = multer({ storage });

// Serve uploaded files
app.use('/uploads', express.static('uploads'));

// Upload endpoint
app.post('/upload', upload.single('file'), (req, res) => {
  if (!req.file) {
    return res.status(400).json({ error: 'No file uploaded' });
  }
  
  const fileUrl = `http://localhost:3000/uploads/${req.file.filename}`;
  
  res.json({
    message: 'File uploaded successfully',
    filename: req.file.filename,
    url: fileUrl
  });
});

app.listen(3000);

Upload request:

curl -F "file=@photo.jpg" http://localhost:3000/upload

Response:

{
  "message": "File uploaded successfully",
  "filename": "1640000000000-123456789.jpg",
  "url": "http://localhost:3000/uploads/1640000000000-123456789.jpg"
}

Access file: Open http://localhost:3000/uploads/1640000000000-123456789.jpg in browser.


Folder-Based Storage Structure

Organizing uploads by category or user:

const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    let folder = 'uploads/';
    
    // Organize by file type
    if (file.mimetype.startsWith('image/')) {
      folder += 'images/';
    } else if (file.mimetype === 'application/pdf') {
      folder += 'documents/';
    } else {
      folder += 'others/';
    }
    
    // Create folder if it doesn't exist
    const fs = require('fs');
    if (!fs.existsSync(folder)) {
      fs.mkdirSync(folder, { recursive: true });
    }
    
    cb(null, folder);
  },
  filename: (req, file, cb) => {
    cb(null, Date.now() + path.extname(file.originalname));
  }
});

Resulting structure:

uploads/
  ├── images/
  │   ├── 1640000000000.jpg
  │   └── 1640000001000.png
  ├── documents/
  │   └── 1640000002000.pdf
  └── others/
      └── 1640000003000.zip

User-Based Organization

destination: (req, file, cb) => {
  const userId = req.user.id; // From authentication middleware
  const userFolder = `uploads/user-${userId}/`;
  
  const fs = require('fs');
  if (!fs.existsSync(userFolder)) {
    fs.mkdirSync(userFolder, { recursive: true });
  }
  
  cb(null, userFolder);
}
uploads/
  ├── user-123/
  │   ├── profile.jpg
  │   └── document.pdf
  └── user-456/
      └── avatar.png

Security Considerations for Uploads

1. File Type Validation

const upload = multer({
  storage: storage,
  fileFilter: (req, file, cb) => {
    // Allow only images
    const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
    
    if (allowedTypes.includes(file.mimetype)) {
      cb(null, true);
    } else {
      cb(new Error('Invalid file type. Only JPEG, PNG, and GIF allowed.'));
    }
  }
});

2. File Size Limits

const upload = multer({
  storage: storage,
  limits: {
    fileSize: 5 * 1024 * 1024 // 5MB limit
  }
});

// Handle size limit errors
app.post('/upload', (req, res) => {
  upload.single('file')(req, res, (err) => {
    if (err instanceof multer.MulterError) {
      if (err.code === 'LIMIT_FILE_SIZE') {
        return res.status(400).json({ error: 'File too large. Max 5MB.' });
      }
    }
    // Handle other errors
  });
});

3. Prevent Path Traversal

const path = require('path');

filename: (req, file, cb) => {
  // Sanitize original filename
  const sanitized = path.basename(file.originalname);
  const uniqueName = Date.now() + '-' + sanitized;
  cb(null, uniqueName);
}

4. Restrict Direct Access

Serve files only to authenticated users:

// DON'T expose uploads directly
// app.use('/uploads', express.static('uploads')); // Insecure!

// DO authenticate before serving
app.get('/files/:filename', authenticateUser, (req, res) => {
  const filename = req.params.filename;
  const filepath = path.join(__dirname, 'uploads', filename);
  
  // Check if user owns this file
  const fileOwner = getFileOwner(filename); // Your logic
  
  if (fileOwner !== req.user.id) {
    return res.status(403).json({ error: 'Access denied' });
  }
  
  res.sendFile(filepath);
});

Best Practices

1. Keep Uploads Out of Version Control

.gitignore:

uploads/
*.log
node_modules/

2. Implement Rate Limiting

const rateLimit = require('express-rate-limit');

const uploadLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 10 // 10 uploads per window
});

app.post('/upload', uploadLimiter, upload.single('file'), (req, res) => {
  // Handle upload
});

3. Log File Operations

app.post('/upload', upload.single('file'), (req, res) => {
  console.log({
    timestamp: new Date(),
    userId: req.user?.id,
    filename: req.file.filename,
    size: req.file.size,
    mimetype: req.file.mimetype
  });
  
  res.json({ message: 'Uploaded successfully' });
});

Key Takeaways

  • Uploaded files can be stored locally (server filesystem) or externally (cloud)

  • Local storage is simple but limited; external storage scales better

  • Use express.static() middleware to serve uploaded files via URLs

  • Organize files in folders by type, user, or date for better management

  • Always validate file types, sizes, and filenames

  • Restrict direct access to sensitive files with authentication

  • Use non-guessable filenames to prevent unauthorized access

  • Keep uploads directory out of version control

  • Implement rate limiting to prevent abuse

  • Consider external storage for production applications