File Uploads with Multer in Express.js

Handling file uploads in web applications

Understanding File Uploads

File uploads are a crucial part of many web applications, allowing users to share images, documents, videos, and other media. Unlike regular form data or JSON, file uploads require special handling due to their binary nature and potential size.

Common File Upload Use Cases

flowchart LR A[Client] -->|multipart/form-data| B[Express Server] B --> C{Multer Middleware} C -->|Process Upload| D[Memory Storage] C -->|Process Upload| E[Disk Storage] C -->|Process Upload| F[Cloud Storage] D & E & F --> G[File Object in Request] G --> H[Application Logic] H --> I[Response to Client]

Think of file uploads like sending a package through the mail instead of a simple letter. The package requires special handling, wrapping, labeling, and might need to go through different processing channels due to its size and contents.

Multer: The Express File Upload Middleware

Multer is a popular Node.js middleware specifically designed for handling multipart/form-data, the format used for file uploads in HTML forms. Multer makes it easy to accept, process, and store uploaded files in Express applications.

Key Multer Features

Basic Multer Setup

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

const app = express();

// Configure multer storage
const storage = multer.diskStorage({
  destination: function(req, file, cb) {
    // Set the destination folder for uploads
    cb(null, 'uploads/');
  },
  filename: function(req, file, cb) {
    // Set the filename to be unique
    const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
    cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname));
  }
});

// Initialize multer with the storage configuration
const upload = multer({ storage: storage });

// Create an endpoint for single file upload
app.post('/upload', upload.single('file'), (req, res) => {
  // req.file contains information about the uploaded file
  if (!req.file) {
    return res.status(400).json({ error: 'No file uploaded' });
  }
  
  // Return information about the uploaded file
  res.json({
    message: 'File uploaded successfully',
    file: {
      filename: req.file.filename,
      originalname: req.file.originalname,
      mimetype: req.file.mimetype,
      size: req.file.size,
      path: req.file.path
    }
  });
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

HTML Form for File Upload

To upload files, your HTML form needs the enctype="multipart/form-data" attribute:

<form action="/upload" method="POST" enctype="multipart/form-data">
  <input type="file" name="file" />
  <button type="submit">Upload</button>
</form>

Multer Configuration Options

Multer provides numerous configuration options to control how files are handled, stored, and validated.

Storage Options

Multer provides several storage strategies for uploaded files:

Disk Storage

const storage = multer.diskStorage({
  // Where to store the files
  destination: function(req, file, cb) {
    cb(null, 'uploads/'); // Files saved to 'uploads' folder
  },
  // How to name the files
  filename: function(req, file, cb) {
    // Customize filename to prevent collisions
    const uniqueName = `${Date.now()}-${Math.round(Math.random() * 1E9)}${path.extname(file.originalname)}`;
    cb(null, uniqueName);
  }
});

Memory Storage

// Store files in memory as Buffer objects
// Useful for small files or when processing without saving
const storage = multer.memoryStorage();

const upload = multer({ storage: storage });

app.post('/process-image', upload.single('image'), (req, res) => {
  // File is available as a buffer in req.file.buffer
  const imageBuffer = req.file.buffer;
  
  // Process the image (e.g., resize with Sharp)
  // processImage(imageBuffer).then(processed => {
  //   // Handle processed image...
  // });
  
  res.json({ message: 'Image processing started' });
});

File Limits and Validation

Control the size and type of files that can be uploaded:

Setting File Limits

const upload = multer({
  storage: multer.diskStorage({/* ... */}),
  limits: {
    fileSize: 5 * 1024 * 1024, // 5MB in bytes
    files: 5 // Max number of files
  },
  fileFilter: function(req, file, cb) {
    // Check file type
    const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
    
    if (!allowedTypes.includes(file.mimetype)) {
      // Reject file
      return cb(new Error('Only JPEG, PNG, and GIF images are allowed'), false);
    }
    
    // Accept file
    cb(null, true);
  }
});
Multer File Upload Processing Client HTML Form multipart/form-data Express Server Route Handler Multer Middleware Validate/Filter Store File Application Processing Logic Response

Upload File Types

Multer provides several methods to handle different file upload scenarios:

Single File Upload

// Handling a single file upload from a field named 'avatar'
app.post('/profile', upload.single('avatar'), (req, res) => {
  // req.file contains information about the uploaded file
  console.log(req.file);
  /* Example output:
  {
    fieldname: 'avatar',
    originalname: 'profile.jpg',
    encoding: '7bit',
    mimetype: 'image/jpeg',
    destination: 'uploads/',
    filename: '1620312345678-293847501.jpg',
    path: 'uploads/1620312345678-293847501.jpg',
    size: 58243
  }
  */
  
  // req.body contains text fields, if any
  console.log(req.body);
  
  res.send('Profile updated!');
});

Multiple Files Upload

// Handling multiple files upload from a field named 'photos'
app.post('/gallery', upload.array('photos', 5), (req, res) => {
  // req.files contains an array of files
  console.log(`Received ${req.files.length} files`);
  
  // Map files to a more user-friendly format
  const uploadedFiles = req.files.map(file => ({
    name: file.originalname,
    type: file.mimetype,
    size: file.size,
    path: file.path
  }));
  
  res.json({
    message: 'Files uploaded successfully!',
    files: uploadedFiles
  });
});

Multiple Fields Upload

// Handling multiple fields with different files
const uploadFields = upload.fields([
  { name: 'avatar', maxCount: 1 },
  { name: 'gallery', maxCount: 5 }
]);

app.post('/profile-complete', uploadFields, (req, res) => {
  // req.files is an object where key is the field name
  console.log(req.files);
  /* Example output:
  {
    avatar: [
      {
        fieldname: 'avatar',
        originalname: 'profile.jpg',
        // ... other file properties
      }
    ],
    gallery: [
      {
        fieldname: 'gallery',
        originalname: 'vacation1.jpg',
        // ... other file properties
      },
      {
        fieldname: 'gallery',
        originalname: 'vacation2.jpg',
        // ... other file properties
      }
      // ... more gallery files
    ]
  }
  */
  
  res.send('Profile updated with gallery!');
});

Error Handling with Multer

Proper error handling is crucial for a good user experience when processing file uploads.

Handling Multer Errors

// Create multer instance with limits and filters
const upload = multer({
  storage: multer.diskStorage({/* ... */}),
  limits: {
    fileSize: 2 * 1024 * 1024 // 2MB
  },
  fileFilter: function(req, file, cb) {
    // Only accept image files
    if (!file.originalname.match(/\.(jpg|jpeg|png|gif)$/)) {
      return cb(new Error('Only image files are allowed!'), false);
    }
    cb(null, true);
  }
});

// Custom error handling middleware
function handleMulterError(err, req, res, next) {
  if (err instanceof multer.MulterError) {
    // A Multer error occurred during upload
    if (err.code === 'LIMIT_FILE_SIZE') {
      return res.status(413).json({
        error: 'File too large',
        message: 'File size exceeds the 2MB limit',
        code: 'FILE_TOO_LARGE'
      });
    } else if (err.code === 'LIMIT_FILE_COUNT') {
      return res.status(400).json({
        error: 'Too many files',
        message: 'You can only upload up to 5 files at once',
        code: 'TOO_MANY_FILES'
      });
    } else {
      // Other Multer errors
      return res.status(400).json({
        error: 'Upload error',
        message: err.message,
        code: err.code
      });
    }
  } else if (err) {
    // Non-Multer error (e.g., from fileFilter)
    return res.status(400).json({
      error: 'File validation error',
      message: err.message || 'Unknown error during file upload'
    });
  }
  
  // No error, proceed to the next middleware
  next();
}

// Use the upload middleware with error handling
app.post('/upload', function(req, res, next) {
  // Use upload.single as middleware, and catch errors
  upload.single('file')(req, res, function(err) {
    // Pass any errors to our handler
    if (err) {
      return handleMulterError(err, req, res, next);
    }
    
    // No errors, process the uploaded file
    if (!req.file) {
      return res.status(400).json({ error: 'No file provided' });
    }
    
    res.json({
      message: 'File uploaded successfully',
      file: req.file.filename
    });
  });
});

Common Multer Error Codes

  • LIMIT_PART_COUNT: Too many parts in the multipart form
  • LIMIT_FILE_SIZE: File size exceeds the configured limit
  • LIMIT_FILE_COUNT: Too many files uploaded
  • LIMIT_FIELD_KEY: Field name too long
  • LIMIT_FIELD_VALUE: Field value too long
  • LIMIT_FIELD_COUNT: Too many fields
  • LIMIT_UNEXPECTED_FILE: Unexpected field name

Advanced File Handling

File Type Validation

It's important to validate file types beyond just checking file extensions, as they can be easily manipulated:

Advanced File Type Validation

const fs = require('fs');
const FileType = require('file-type');

// File filter that checks actual file contents
const fileFilter = async (req, file, cb) => {
  try {
    // Check mimetype first (basic check)
    const allowedMimes = ['image/jpeg', 'image/png', 'image/gif'];
    if (!allowedMimes.includes(file.mimetype)) {
      return cb(new Error('File type not allowed'), false);
    }
    
    // For more security, we could analyze the file buffer
    // This requires storing the file first, or using memoryStorage
    // Example with memoryStorage:
    /* 
    // Read the first few bytes to determine file type
    const buffer = file.buffer;
    const fileTypeResult = await FileType.fromBuffer(buffer);
    
    if (!fileTypeResult || !allowedMimes.includes(fileTypeResult.mime)) {
      return cb(new Error('Invalid file content type'), false);
    }
    */
    
    // File passed validation
    cb(null, true);
  } catch (err) {
    cb(new Error('Error validating file type'), false);
  }
};

const upload = multer({
  storage: multer.diskStorage({/* ... */}),
  fileFilter: fileFilter
});

Handling File Processing

Often, you'll need to process uploaded files before storing them permanently:

Image Processing with Sharp

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

const app = express();

// Use memory storage for processing before saving
const upload = multer({
  storage: multer.memoryStorage(),
  limits: { fileSize: 5 * 1024 * 1024 }, // 5MB
  fileFilter: (req, file, cb) => {
    if (file.mimetype.startsWith('image/')) {
      cb(null, true);
    } else {
      cb(new Error('Only images are allowed'), false);
    }
  }
});

app.post('/upload-profile', upload.single('avatar'), async (req, res) => {
  try {
    if (!req.file) {
      return res.status(400).json({ error: 'No image uploaded' });
    }
    
    // Get uploaded file buffer
    const imageBuffer = req.file.buffer;
    const filename = Date.now() + '.webp';
    const outputPath = path.join('uploads', filename);
    
    // Process the image: resize to 300x300, convert to WebP format
    await sharp(imageBuffer)
      .resize(300, 300, {
        fit: 'cover',
        position: 'center'
      })
      .webp({ quality: 80 })
      .toFile(outputPath);
    
    // Generate a thumbnail version as well
    await sharp(imageBuffer)
      .resize(100, 100, {
        fit: 'cover',
        position: 'center'
      })
      .webp({ quality: 70 })
      .toFile(path.join('uploads', 'thumb_' + filename));
    
    // Return success with file info
    res.json({
      message: 'Profile image uploaded and processed',
      image: {
        url: `/images/${filename}`,
        thumbnail: `/images/thumb_${filename}`
      }
    });
  } catch (error) {
    console.error('Error processing image:', error);
    res.status(500).json({
      error: 'Failed to process image',
      details: error.message
    });
  }
});

Storing Upload Metadata

In real applications, you'll likely store file metadata in a database:

Storing File Metadata in Database

// Pseudo-code example with a fictional database client
const express = require('express');
const multer = require('multer');
const db = require('./database'); // Your database client
const app = express();

const storage = multer.diskStorage({/* ... */});
const upload = multer({ storage });

app.post('/documents', upload.single('document'), async (req, res) => {
  try {
    if (!req.file) {
      return res.status(400).json({ error: 'No document uploaded' });
    }
    
    // Get the uploaded file info
    const { originalname, mimetype, filename, size, path } = req.file;
    
    // Get additional metadata from the request body
    const { title, description, category } = req.body;
    
    // Store metadata in database
    const fileId = await db.files.create({
      originalName: originalname,
      storedName: filename,
      mimeType: mimetype,
      size: size,
      path: path,
      title: title || originalname,
      description: description || '',
      category: category || 'uncategorized',
      uploadedBy: req.user.id, // Assuming authenticated user
      uploadedAt: new Date(),
      status: 'active'
    });
    
    // Return success response with file ID
    res.status(201).json({
      message: 'Document uploaded successfully',
      fileId: fileId,
      title: title || originalname,
      url: `/documents/${fileId}`
    });
  } catch (error) {
    console.error('Error storing document metadata:', error);
    res.status(500).json({
      error: 'Failed to complete document upload',
      details: error.message
    });
  }
});

Cloud Storage Integration

For production applications, storing files in cloud services like AWS S3, Google Cloud Storage, or Azure Blob Storage is often preferred over local storage.

Multer with AWS S3 Storage

const express = require('express');
const multer = require('multer');
const multerS3 = require('multer-s3');
const { S3Client } = require('@aws-sdk/client-s3');

const app = express();

// Configure AWS S3 client
const s3 = new S3Client({
  region: 'us-east-1',
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
  }
});

// Configure multer to use S3 storage
const upload = multer({
  storage: multerS3({
    s3: s3,
    bucket: 'my-app-uploads',
    acl: 'public-read', // Make files public
    contentType: multerS3.AUTO_CONTENT_TYPE, // Set content type based on file
    metadata: function (req, file, cb) {
      // Set custom metadata
      cb(null, { 
        fieldName: file.fieldname,
        uploadedBy: req.user ? req.user.id : 'anonymous'
      });
    },
    key: function (req, file, cb) {
      // Set file path and name in S3
      const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
      const key = `uploads/${req.user ? req.user.id : 'public'}/${uniqueSuffix}-${file.originalname}`;
      cb(null, key);
    }
  }),
  limits: { fileSize: 10 * 1024 * 1024 } // 10MB
});

app.post('/upload-to-s3', upload.single('file'), (req, res) => {
  if (!req.file) {
    return res.status(400).json({ error: 'No file uploaded' });
  }
  
  // File upload success, file information in req.file
  res.json({
    message: 'File uploaded to S3 successfully',
    file: {
      url: req.file.location, // S3 URL to the file
      key: req.file.key,
      size: req.file.size,
      mimetype: req.file.mimetype
    }
  });
});

Using cloud storage provides several benefits:

Security Considerations

File uploads present several security challenges that need to be addressed:

flowchart TD A[File Upload Security] --> B[File Type Validation] A --> C[Size Limitations] A --> D[File Scanning] A --> E[Storage Isolation] A --> F[Secure File Names] A --> G[Access Controls] B --> B1[Verify MIME types] B --> B2[Check file signatures] B --> B3[Content validation] C --> C1[Prevent DoS attacks] C --> C2[Protect storage capacity] D --> D1[Virus scanning] D --> D2[Malware detection] E --> E1[Separate upload directory] E --> E2[Prevent code execution] F --> F1[Sanitize filenames] F --> F2[Use random IDs] G --> G1[Authentication] G --> G2[Authorization] G --> G3[Signed URLs]

Security Best Practices Implementation

const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const crypto = require('crypto');
const { promisify } = require('util');
const fileTypeFromBuffer = require('file-type').fromBuffer;

const app = express();

// Create upload directory if it doesn't exist
const uploadDir = path.join(__dirname, 'uploads');
if (!fs.existsSync(uploadDir)) {
  fs.mkdirSync(uploadDir, { recursive: true });
}

// Sanitize filename to prevent path traversal
function sanitizeFilename(filename) {
  // Remove path components
  const sanitized = path.basename(filename);
  // Remove dangerous characters
  return sanitized.replace(/[^a-zA-Z0-9_.-]/g, '_');
}

// Generate a secure random filename
async function generateSecureFilename(originalname) {
  const randomBytes = await promisify(crypto.randomBytes)(16);
  const extension = path.extname(originalname).toLowerCase();
  return `${randomBytes.toString('hex')}${extension}`;
}

// Configure storage
const storage = multer.diskStorage({
  destination: function(req, file, cb) {
    cb(null, uploadDir);
  },
  filename: async function(req, file, cb) {
    try {
      const secureName = await generateSecureFilename(file.originalname);
      cb(null, secureName);
    } catch (err) {
      cb(err);
    }
  }
});

// Configure file filter
const fileFilter = async function(req, file, cb) {
  try {
    // Whitelist of allowed MIME types
    const allowedMimes = ['image/jpeg', 'image/png', 'application/pdf'];
    
    if (!allowedMimes.includes(file.mimetype)) {
      return cb(new Error('File type not allowed'), false);
    }
    
    // Additional validation could be performed here
    // For example, if using memoryStorage, check the actual file contents
    
    cb(null, true);
  } catch (err) {
    cb(err);
  }
};

// Configure limits
const limits = {
  fileSize: 5 * 1024 * 1024, // 5MB
  files: 1 // Only one file at a time
};

const upload = multer({ 
  storage: storage,
  fileFilter: fileFilter,
  limits: limits
});

// Secure upload endpoint
app.post('/secure-upload', async (req, res) => {
  try {
    // Handle the upload with custom error handling
    upload.single('file')(req, res, async function(err) {
      if (err) {
        console.error('Upload error:', err);
        return res.status(400).json({
          error: err.code || 'UPLOAD_ERROR',
          message: err.message || 'Error uploading file'
        });
      }
      
      // Check if file was uploaded
      if (!req.file) {
        return res.status(400).json({
          error: 'NO_FILE',
          message: 'No file was uploaded'
        });
      }
      
      // Verify file content type for extra security (if using memoryStorage)
      // if (req.file.buffer) {
      //   const fileInfo = await fileTypeFromBuffer(req.file.buffer);
      //   if (!fileInfo || !allowedMimes.includes(fileInfo.mime)) {
      //     // Delete the file if its content doesn't match allowed types
      //     fs.unlinkSync(req.file.path);
      //     return res.status(400).json({
      //       error: 'INVALID_CONTENT',
      //       message: 'File content type not allowed'
      //     });
      //   }
      // }
      
      // Set permissions on the uploaded file
      fs.chmodSync(req.file.path, 0o640); // Owner: read/write, Group: read, Others: none
      
      // Log the upload (in production, store in database)
      console.log(`File uploaded: ${req.file.originalname} → ${req.file.filename}`);
      
      // Return success with limited file info (don't expose full paths)
      res.json({
        success: true,
        file: {
          name: req.file.originalname,
          size: req.file.size,
          type: req.file.mimetype,
          // Use a URL that doesn't expose the physical path
          url: `/files/${req.file.filename}`
        }
      });
    });
  } catch (error) {
    console.error('Unexpected error:', error);
    res.status(500).json({
      error: 'SERVER_ERROR',
      message: 'An unexpected error occurred'
    });
  }
});

// Serve uploaded files securely
app.get('/files/:filename', (req, res) => {
  const filename = sanitizeFilename(req.params.filename);
  const filePath = path.join(uploadDir, filename);
  
  // Check if file exists
  if (!fs.existsSync(filePath)) {
    return res.status(404).json({ error: 'File not found' });
  }
  
  // Optional: Check user authorization to access this file
  // if (!userCanAccessFile(req.user, filename)) {
  //   return res.status(403).json({ error: 'Access denied' });
  // }
  
  // Set proper headers and send file
  res.sendFile(filePath);
});

Key Security Recommendations

  • Validate File Types - Check both MIME types and file contents
  • Enforce Size Limits - Prevent server overload and DoS attacks
  • Use Secure Filenames - Randomize names and prevent path traversal
  • Store Files Securely - Outside of web root with proper permissions
  • Scan for Malware - Use virus scanning services in production
  • Implement Access Controls - Ensure only authorized users can access uploaded files
  • Consider Cloud Storage - Let specialized services handle security

Real-World Application: Media Upload Service

Let's build a more comprehensive example of a media upload service that handles different types of files:

Complete Media Upload Service

const express = require('express');
const multer = require('multer');
const sharp = require('sharp');
const path = require('path');
const fs = require('fs');
const crypto = require('crypto');
const { promisify } = require('util');

const app = express();
app.use(express.json());

// Create required directories
const baseUploadDir = path.join(__dirname, 'uploads');
const imageDir = path.join(baseUploadDir, 'images');
const documentDir = path.join(baseUploadDir, 'documents');
const videoDir = path.join(baseUploadDir, 'videos');

[baseUploadDir, imageDir, documentDir, videoDir].forEach(dir => {
  if (!fs.existsSync(dir)) {
    fs.mkdirSync(dir, { recursive: true });
  }
});

// Generate a secure random filename
async function generateSecureFilename(originalname) {
  const randomBytes = await promisify(crypto.randomBytes)(16);
  const extension = path.extname(originalname).toLowerCase();
  return `${randomBytes.toString('hex')}${extension}`;
}

// Configure storage strategy based on file type
const storage = multer.diskStorage({
  destination: function(req, file, cb) {
    let uploadDir;
    
    if (file.mimetype.startsWith('image/')) {
      uploadDir = imageDir;
    } else if (file.mimetype.startsWith('video/')) {
      uploadDir = videoDir;
    } else {
      uploadDir = documentDir;
    }
    
    cb(null, uploadDir);
  },
  filename: async function(req, file, cb) {
    try {
      const secureName = await generateSecureFilename(file.originalname);
      cb(null, secureName);
    } catch (err) {
      cb(err);
    }
  }
});

// Configure file filter based on allowed types
const fileFilter = function(req, file, cb) {
  // Define allowed MIME types
  const allowedTypes = {
    image: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
    document: ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'text/plain', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],
    video: ['video/mp4', 'video/quicktime', 'video/x-msvideo']
  };
  
  // Check upload type from request
  const uploadType = req.params.type || req.query.type || 'image';
  
  // Get allowed MIME types for this upload type
  const allowed = allowedTypes[uploadType] || allowedTypes.image;
  
  if (!allowed.includes(file.mimetype)) {
    return cb(new Error(`File type not allowed. Allowed types: ${allowed.join(', ')}`), false);
  }
  
  cb(null, true);
};

// Set limits based on file type
function getLimits(uploadType) {
  const limits = {
    image: { fileSize: 5 * 1024 * 1024 }, // 5MB
    document: { fileSize: 10 * 1024 * 1024 }, // 10MB
    video: { fileSize: 100 * 1024 * 1024 }  // 100MB
  };
  
  return limits[uploadType] || limits.image;
}

// Create upload middleware function based on request type
function createUploadMiddleware(req, res, next) {
  const uploadType = req.params.type || req.query.type || 'image';
  
  const upload = multer({
    storage: storage,
    fileFilter: fileFilter,
    limits: getLimits(uploadType)
  });
  
  return upload.single('file');
}

// Media upload endpoint with dynamic type
app.post('/upload/:type?', (req, res, next) => {
  const uploadMiddleware = createUploadMiddleware(req, res, next);
  
  uploadMiddleware(req, res, async function(err) {
    if (err) {
      console.error('Upload error:', err);
      return res.status(400).json({
        success: false,
        error: err.code || 'UPLOAD_ERROR',
        message: err.message || 'Error uploading file'
      });
    }
    
    if (!req.file) {
      return res.status(400).json({
        success: false,
        error: 'NO_FILE',
        message: 'No file was uploaded'
      });
    }
    
    try {
      // Get upload type
      const uploadType = req.params.type || req.query.type || 'image';
      
      // Process based on file type
      if (uploadType === 'image' && req.file.mimetype.startsWith('image/')) {
        // Process image (resize, create thumbnails, etc.)
        await processImage(req.file);
      } else if (uploadType === 'document') {
        // Process document (e.g., generate preview)
        // processDocument(req.file);
      } else if (uploadType === 'video') {
        // Process video (e.g., generate thumbnail)
        // processVideo(req.file);
      }
      
      // Create media record
      const media = {
        id: crypto.randomBytes(8).toString('hex'),
        originalName: req.file.originalname,
        fileName: req.file.filename,
        fileSize: req.file.size,
        mimeType: req.file.mimetype,
        fileType: uploadType,
        uploadedAt: new Date(),
        path: req.file.path,
        url: `/media/${uploadType}/${req.file.filename}`
      };
      
      // Store media record (in a real app, this would go to a database)
      // await mediaService.create(media);
      
      // Respond with success
      res.status(201).json({
        success: true,
        message: 'File uploaded successfully',
        media: {
          id: media.id,
          name: media.originalName,
          type: media.fileType,
          size: media.fileSize,
          url: media.url,
          uploadedAt: media.uploadedAt
        }
      });
    } catch (error) {
      console.error('Processing error:', error);
      res.status(500).json({
        success: false,
        error: 'PROCESSING_ERROR',
        message: 'Error processing uploaded file'
      });
    }
  });
});

// Image processing function
async function processImage(file) {
  try {
    // Only process images
    if (!file.mimetype.startsWith('image/')) {
      return;
    }
    
    const filename = path.basename(file.filename, path.extname(file.filename));
    const outputDir = path.dirname(file.path);
    
    // Create thumbnail version
    await sharp(file.path)
      .resize(200, 200, { fit: 'cover' })
      .jpeg({ quality: 80 })
      .toFile(path.join(outputDir, `${filename}_thumb.jpg`));
    
    // Create medium version
    await sharp(file.path)
      .resize(800, 800, { fit: 'inside', withoutEnlargement: true })
      .jpeg({ quality: 85 })
      .toFile(path.join(outputDir, `${filename}_medium.jpg`));
    
    console.log('Image processed successfully');
  } catch (error) {
    console.error('Error processing image:', error);
    throw error;
  }
}

// Serve media files
app.get('/media/:type/:filename', (req, res) => {
  const { type, filename } = req.params;
  
  // Validate type and filename to prevent path traversal
  const validTypes = ['images', 'documents', 'videos'];
  if (!validTypes.includes(type)) {
    return res.status(400).json({ error: 'Invalid media type' });
  }
  
  const sanitizedFilename = path.basename(filename);
  const filePath = path.join(baseUploadDir, type, sanitizedFilename);
  
  // Check if file exists
  if (!fs.existsSync(filePath)) {
    return res.status(404).json({ error: 'File not found' });
  }
  
  // Send file with appropriate headers
  res.sendFile(filePath);
});

app.listen(3000, () => {
  console.log('Media upload service running on port 3000');
});

Practical Exercises

Exercise 1: Basic File Upload

Create a simple Express application that:

  1. Accepts image uploads (only JPEG and PNG)
  2. Stores files in an 'uploads' folder with unique names
  3. Limits file size to 2MB
  4. Returns file information after upload
  5. Handles and displays appropriate error messages

Include a simple HTML form for testing the upload functionality.

Exercise 2: Advanced Media Library

Create an Express application that implements a media library with the following features:

  1. Handles different file types (images, documents, videos)
  2. Processes images to create multiple sizes (original, medium, thumbnail)
  3. Stores file metadata in a JSON file (simulating a database)
  4. Provides endpoints to:
    • Upload files
    • List all uploaded files
    • Get file details by ID
    • Delete files
  5. Implements proper security measures

Bonus: Add a simple front-end interface for the media library.

Additional Resources

Documentation

Articles and Tutorials

Related Packages