Have any questions:

Toll free:9801887718Available 24/7

Email our experts:info@mantraideas.com

In: Web Development

Introduction

File uploads are a cornerstone of modern web applications, powering features like user profile images, document sharing, and media galleries. This guide provides a step-by-step approach to implementing secure, scalable, and efficient file uploads using Node.js, Express, Multer, and Cloudinary. Whether you’re building a social media platform, e-commerce site, or CMS, this solution ensures robust file handling with minimal server overhead.

Why This Stack?

Multer: A middleware for handling multipart/form-data, optimized for file uploads using the efficient busboy library.
Cloudinary: A cloud-based service for secure file uploads, storage, optimization, and delivery via a global CDN.
Benefits of this approach:

  • Scalable cloud storage
  • Automatic image optimization
  • Built-in transformations (resize, crop, etc.)
  • Fast delivery via CDN
  • Enhanced security features
  • No local server storage constraints


Prerequisites

To follow this guide, you need:

  • Node.js (v14 or higher)
  • Basic knowledge of Express.js
  • A Cloudinary account (free tier available)
  • Familiarity with middleware concepts

Project Setup

Alright enough of the Project discussion and requirements.How about we actually dive into implementing it? Yup let’s deep dive with the actual implementation with setting up our environment

Step 1: Initialize the Project
Create a new project directory and initialize it:

mkdir file-upload-api
cd file-upload-api
npm init -y

Step 2: Install Dependencies

npm install express multer cloudinary dotenv cors helmet morgan npm install -D nodemon

Step 3: Project Structure


Configuration

Step 4: Set Up Environment Variables

Create a .env file in the project root:

PORT=3000
NODE_ENV=development

# Cloudinary Configuration
CLOUDINARY_CLOUD_NAME=your_cloud_name
CLOUDINARY_API_KEY=your_api_key
CLOUDINARY_API_SECRET=your_api_secret,

# Upload Configuration
MAX_FILE_SIZE=5242880
ALLOWED_FILE_TYPES=jpeg,jpg,png,gif,pdf,doc,docx

Step 5: Cloudinary Setup

Create utils/cloudinary.js:

const { v2: cloudinary } = require('cloudinary');

cloudinary.config({
  cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
  api_key: process.env.CLOUDINARY_API_KEY,
  api_secret: process.env.CLOUDINARY_API_SECRET,
});

/**
 * Uploads a file to Cloudinary
 * @param {string} filePath - Local file path
 * @param {object} options - Upload options
 * @returns {Promise<object>} - Upload result
 */
const uploadToCloudinary = async (filePath, options = {}) => {
  try {
    const result = await cloudinary.uploader.upload(filePath, {
      folder: 'uploads',
      resource_type: 'auto',
      use_filename: true,
      unique_filename: false,
      ...options,
    });
    return {
      success: true,
      data: {
        public_id: result.public_id,
        url: result.secure_url,
        format: result.format,
        resource_type: result.resource_type,
        bytes: result.bytes,
        width: result.width || null,
        height: result.height || null,
        created_at: result.created_at,
      },
    };
  } catch (error) {
    console.error('Cloudinary upload error:', error);
    return { success: false, error: error.message };
  }
};

/**
 * Deletes a file from Cloudinary
 * @param {string} publicId - File's public ID
 * @param {string} resourceType - Resource type (e.g., image, video)
 * @returns {Promise<object>} - Deletion result
 */
const deleteFromCloudinary = async (publicId, resourceType = 'image') => {
  try {
    const result = await cloudinary.uploader.destroy(publicId, { resource_type: resourceType });
    return { success: result.result === 'ok', result: result.result };
  } catch (error) {
    console.error('Cloudinary deletion error:', error);
    return { success: false, error: error.message };
  }
};

/**
 * Generates an optimized image URL
 * @param {string} publicId - Image's public ID
 * @param {object} transformations - Transformation options
 * @returns {string} - Optimized URL
 */
const getOptimizedUrl = (publicId, transformations = {}) => {
  return cloudinary.url(publicId, {
    quality: 'auto',
    fetch_format: 'auto',
    ...transformations,
  });
};

module.exports = { uploadToCloudinary, deleteFromCloudinary, getOptimizedUrl, cloudinary };

Multer Configuration

6. Set Up Multer Middleware

Create middleware/upload.js to configure Multer:

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

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

const storage = multer.diskStorage({
  destination: (req, file, cb) => cb(null, uploadDir),
  filename: (req, file, cb) => {
    const uniqueSuffix = `${Date.now()}-${Math.round(Math.random() * 1e9)}`;
    const extension = path.extname(file.originalname);
    cb(null, `${file.fieldname}-${uniqueSuffix}${extension}`);
  },
});

const fileFilter = (req, file, cb) => {
  const allowedTypes = process.env.ALLOWED_FILE_TYPES.split(',');
  const mimeType = file.mimetype;
  if (allowedTypes.includes(mimeType)) {
    cb(null, true);
  } else {
    cb(new Error(`File type ${mimeType} not allowed. Allowed types: ${allowedTypes.join(', ')}`), false);
  }
};

const upload = multer({
  storage,
  limits: {
    fileSize: parseInt(process.env.MAX_FILE_SIZE) || 5 * 1024 * 1024, // 5MB
    files: 5, // Max 5 files
  },
  fileFilter,
});

const handleUploadError = (err, req, res, next) => {
  if (err instanceof multer.MulterError) {
    const messages = {
      LIMIT_FILE_SIZE: 'File too large. Maximum size is 5MB.',
      LIMIT_FILE_COUNT: 'Too many files. Maximum 5 files allowed.',
      LIMIT_UNEXPECTED_FILE: 'Unexpected file field.',
    };
    return res.status(400).json({ success: false, message: messages[err.code] || 'File upload error.' });
  } else if (err) {
    return res.status(400).json({ success: false, message: err.message });
  }
  next();
};

module.exports = { upload, handleUploadError };

Controller Implementation

Step 7: Upload Controller

Create controllers/uploadController.js:

const fs = require('fs').promises;
const { uploadToCloudinary, deleteFromCloudinary, getOptimizedUrl } = require('../utils/cloudinary');

const uploadSingleFile = async (req, res) => {
  try {
    if (!req.file) {
      return res.status(400).json({ success: false, message: 'No file uploaded' });
    }
    const { path: filePath } = req.file;
    const uploadResult = await uploadToCloudinary(filePath, { folder: 'single-uploads' });
    await fs.unlink(filePath);
    if (!uploadResult.success) {
      return res.status(500).json({ success: false, message: 'Failed to upload file', error: uploadResult.error });
    }
    res.status(200).json({ success: true, message: 'File uploaded successfully', data: uploadResult.data });
  } catch (error) {
    console.error('Upload error:', error);
    res.status(500).json({ success: false, message: 'Internal server error', error: error.message });
  }
};

const uploadMultipleFiles = async (req, res) => {
  try {
    if (!req.files?.length) {
      return res.status(400).json({ success: false, message: 'No files uploaded' });
    }
    const uploadPromises = req.files.map(async (file) => {
      try {
        const uploadResult = await uploadToCloudinary(file.path, { folder: 'multiple-uploads' });
        await fs.unlink(file.path);
        if (uploadResult.success) {
          return { originalName: file.originalname, ...uploadResult.data };
        }
        throw new Error(uploadResult.error);
      } catch (error) {
        await fs.unlink(file.path).catch((unlinkError) => console.error('Error removing temporary file:', unlinkError));
        throw error;
      }
    });
    const results = await Promise.allSettled(uploadPromises);
    const successfulUploads = results.filter(r => r.status === 'fulfilled').map(r => r.value);
    const failedUploads = results.filter(r => r.status === 'rejected').map(r => r.reason.message);
    res.status(200).json({
      success: true,
      message: `${successfulUploads.length} files uploaded successfully`,
      data: { successful: successfulUploads, failed: failedUploads, total: req.files.length },
    });
  } catch (error) {
    console.error('Multiple upload error:', error);
    res.status(500).json({ success: false, message: 'Internal server error', error: error.message });
  }
};

const getOptimizedImage = async (req, res) => {
  try {
    const { publicId } = req.params;
    const { width, height, quality, format } = req.query;
    if (!publicId) {
      return res.status(400).json({ success: false, message: 'Public ID is required' });
    }
    const transformations = { width, height, quality, fetch_format: format }.filter(Boolean);
    const optimizedUrl = getOptimizedUrl(publicId, transformations);
    res.status(200).json({ success: true, message: 'Optimized URL generated', data: { public_id: publicId, optimized_url: optimizedUrl, transformations } });
  } catch (error) {
    console.error('Optimization error:', error);
    res.status(500).json({ success: false, message: 'Internal server error', error: error.message });
  }
};

const deleteFile = async (req, res) => {
  try {
    const { publicId } = req.params;
    const { resourceType = 'image' } = req.query;
    if (!publicId) {
      return res.status(400).json({ success: false, message: 'Public ID is required' });
    }
    const deleteResult = await deleteFromCloudinary(publicId, resourceType);
    if (!deleteResult.success) {
      return res.status(500).json({ success: false, message: 'Failed to delete file', error: deleteResult.error });
    }
    res.status(200).json({ success: true, message: 'File deleted successfully', data: deleteResult });
  } catch (error) {
    console.error('Delete error:', error);
    res.status(500).json({ success: false, message: 'Internal server error', error: error.message });
  }
};

module.exports = { uploadSingleFile, uploadMultipleFiles, getOptimizedImage, deleteFile };


Routes Setup

8. Define Upload Routes

Create routes/upload.js:

const express = require('express');
const { upload, handleUploadError } = require('../middleware/upload');
const { uploadSingleFile, uploadMultipleFiles, getOptimizedImage, deleteFile } = require('../controllers/uploadController');

const router = express.Router();

router.post('/single', upload.single('file'), handleUploadError, uploadSingleFile);
router.post('/multiple', upload.array('files', 5), handleUploadError, uploadMultipleFiles);
router.get('/optimize/:publicId', getOptimizedImage);
router.delete('/:publicId', deleteFile);

module.exports = router;

Application Setup

9. Configure Express Application

Create app.js:

const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
require('dotenv').config();

const uploadRoutes = require('./routes/upload');

const app = express();

app.use(helmet());
app.use(cors({
  origin: process.env.NODE_ENV === 'production' ? ['https://yourdomain.com'] : ['http://localhost:3000', 'http://localhost:3001'],
  credentials: true,
}));
app.use(morgan('combined'));
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));

app.use('/api/upload', uploadRoutes);

app.get('/health', (req, res) => {
  res.status(200).json({ success: true, message: 'Server is running', timestamp: new Date().toISOString() });
});

app.use('*', (req, res) => {
  res.status(404).json({ success: false, message: 'Route not found' });
});

app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({
    success: false,
    message: 'Something went wrong!',
    error: process.env.NODE_ENV === 'development' ? err.message : undefined,
  });
});

module.exports = app;

Step 10: Server Entry Point

Create server.js:

const app = require('./app');

const PORT = process.env.PORT || 3000;

const server = app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
  console.log(`Environment: ${process.env.NODE_ENV}`);
});

process.on('SIGTERM', () => {
  console.log('SIGTERM received, shutting down gracefully');
  server.close(() => console.log('Process terminated'));
});

process.on('SIGINT', () => {
  console.log('SIGINT received, shutting down gracefully');
  server.close(() => console.log('Process terminated'));
});


Testing the API

Step 11: Testing with cURL

# Single file upload
curl -X POST \
  -F "file=@/path/to/your/image.jpg" \
  http://localhost:3000/api/upload/single

# Multiple files upload
curl -X POST \
  -F "files=@/path/to/image1.jpg" \
  -F "files=@/path/to/image2.jpg" \
  http://localhost:3000/api/upload/multiple

# Get optimized image URL
curl -X GET \
  "http://localhost:3000/api/upload/optimize/your-public-id?width=300&height=300&quality=auto"

# Delete file
curl -X DELETE \
  http://localhost:3000/api/upload/your-public-id

Congratulations!! You have successfully completed the API creation part. Now let’s dive into the Frontend Integration.

12. Frontend Integration Example
Create a simple HTML page for testing:
Html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>File Upload Demo</title>
</head>
<body>
  <h1>File Upload Demo</h1>

  <form id="singleUpload" enctype="multipart/form-data">
    <input type="file" name="file" accept="image/*,.pdf,.doc,.docx" required>
    <button type="submit">Upload Single File</button>
  </form>

  <form id="multipleUpload" enctype="multipart/form-data">
    <input type="file" name="files" accept="image/*,.pdf,.doc,.docx" multiple required>
    <button type="submit">Upload Multiple Files</button>
  </form>

  <div id="result"></div>

  <script>
    const resultDiv = document.getElementById('result');

    document.getElementById('singleUpload').addEventListener('submit', async (e) => {
      e.preventDefault();
      const formData = new FormData(e.target);
      try {
        const response = await fetch('/api/upload/single', { method: 'POST', body: formData });
        const result = await response.json();
        resultDiv.innerHTML = `<pre>${JSON.stringify(result, null, 2)}</pre>`;
      } catch (error) {
        resultDiv.innerHTML = `<p>Error: ${error.message}</p>`;
      }
    });

    document.getElementById('multipleUpload').addEventListener('submit', async (e) => {
      e.preventDefault();
      const formData = new FormData(e.target);
      try {
        const response = await fetch('/api/upload/multiple', { method: 'POST', body: formData });
        const result = await response.json();
        resultDiv.innerHTML = `<pre>${JSON.stringify(result, null, 2)}</pre>`;
      } catch (error) {
        resultDiv.innerHTML = `<p>Error: ${error.message}</p>`;
      }
    });
  </script>
</body>
</html>

Conclusion

This guide provides a production-ready solution for file uploads using Node.js, Express, Multer, and Cloudinary. Key features include:

  • Secure file validation and handling
  • Scalable cloud storage with Cloudinary
  • Automatic image optimization
  • Comprehensive error handling and logging
  • Performance and security best practices


Thank you for taking the time to read this article.

I hope it has provided you with clear guidance and practical insights for implementing file upload functionality in your application. Should you have any questions or feedback, I welcome you to share them. Your engagement is greatly appreciated.

Spread the love

Leave a Reply

Your email address will not be published. Required fields are marked *