MEDIA GUIDES / Image Effects

Learning How to Upload Multiple Files with JavaScript

Allowing users to upload multiple files at once has become more of a need in web development, rather than a nice-to-have feature. Multiple file upload greatly improves user experience by simplifying tasks that involve handling numerous files. Some of its applications include allowing users to upload multiple photos or videos simultaneously to create an album in social media apps, uploading several resources at once in file storage and transfer services, and more.

In this guide, you’ll learn:

Basics of Uploading Many Files at Once

Uploading multiple files means sending more than one file from your browser to a server in a single action. This is done using HTML forms and JavaScript to package the files and send them via a POST request.

Why is this useful? Imagine building a social media app where users can upload several photos from their gallery. Without multiple uploads, they’d have to select and send each photo separately, which is slow and frustrating. With JavaScript, you can let users pick multiple files by holding Ctrl (or Cmd on Mac) while clicking, or by dragging and dropping.

In the next section, we’ll walk you through how to implement multiple file upload in Node.js, using Express and Multer, a Node.js middleware for handling multipart/form-data uploads in web applications.

Setting up the Project

First of all, you’ll need to install Node.js. If you don’t have it, you can download and install it from its official website. Next, create a project directory and run the following command to initialize a new Node project:

npm init -y

Next, install the project dependencies using the command below:

npm install express multer

We’re using Multer as a part of this tutorial. It’s a middleware for handling form data, making things easier for us than coding all of the upload logic ourselves.

Create Express Server

Next, we’ll create a server to handle the backend logic for the application using Express.

In the project root, create a file named server.js and add the following code to it:

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

const app = express();
const PORT = process.env.PORT || 3000;

// Serve static files
app.use(express.static(path.join(__dirname, 'public')));

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

The code above creates an Express server and allows us to serve static files like HTML and JavaScript from the public folder, which we’ll create in a moment.

To start the application, run node server.js in the root directory. If successful, you’ll see “Server running on http://localhost:3000” in the console.

Create HTML Form for File Upload

Now that we have a server running, we need a user interface where users can upload files from.

Create a folder named public in the project root, inside it, create a file called index.html and add the following code to it:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Multiple File Upload</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <style>
      .upload-area {
        position: relative;
        cursor: pointer;
      }
      .upload-label {
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        display: flex;
        align-items: center;
        justify-content: center;
        pointer-events: none;
        text-align: center;
      }
      #fileInput {
        opacity: 0;
        height: 100%;
        width: 100%;
        cursor: pointer;
      }
    </style>
  </head>
  <body class="bg-gray-100 flex items-center justify-center min-h-screen p-4">
    <div
      class="container bg-white p-8 rounded-xl shadow-lg w-full max-w-2xl mx-auto"
    >
      <h1 class="text-3xl font-bold text-center text-gray-800 mb-6">
        Upload Multiple Files
      </h1>

      <form id="uploadForm" enctype="multipart/form-data" class="space-y-6">
        <div
          class="upload-area border-2 border-dashed border-gray-300 rounded-lg p-10 text-gray-500 hover:border-blue-500 hover:text-blue-500 transition duration-300 ease-in-out"
        >
          <input
            type="file"
            id="fileInput"
            name="files"
            multiple
            accept="image/*"
            class="hidden"
          />
          <label for="fileInput" class="upload-label">
            <span class="text-center">
              <svg
                xmlns="http://www.w3.org/2000/svg"
                class="mx-auto h-10 w-10 mb-2"
                viewBox="0 0 24 24"
                fill="none"
                stroke="currentColor"
                stroke-width="1.8"
                stroke-linecap="round"
                stroke-linejoin="round"
                aria-hidden="true"
              >
                <title>Upload</title>
                <path d="M4 16v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2" />
                <path d="M12 4v12" />
                <path d="M8 8l4-4 4 4" />
              </svg>

              <span class="font-semibold text-lg"
                >Choose Files or Drag & Drop</span
              >
            </span>
          </label>
        </div>

        <div id="fileList" class="file-list space-y-2"></div>

        <button
          type="submit"
          id="uploadBtn"
          disabled
          class="w-full bg-blue-500 text-white font-bold py-3 px-4 rounded-lg hover:bg-blue-600 transition-colors duration-300 disabled:bg-gray-400 disabled:cursor-not-allowed"
        >
          Upload Files
        </button>
      </form>

      <div id="progressContainer" class="progress-container mt-6"></div>
      <div
        id="results"
        class="results mt-4 p-4 bg-gray-50 rounded-lg hidden"
      ></div>
    </div>

    <script src="script.js"></script>
  </body>
</html>

The key feature that enables users to select and upload multiple files simultaneously is the multiple attribute on the file input element. Setting type="file" tells the browser that the input should be used for file uploads. Additionally, the accept attribute can be used to restrict the types of files that can be selected–for example, limiting input to image files only.

This is what the page looks like when you navigate to localhost:3000:

So far the form does not do anything but you’’ll notice we added this line <script src="script.js"></script> in the HTML: this will be used to add JavaScript code to make the form interactive so we can upload files to our Express server.

Create JavaScript to Handle Form Upload

Create a file named script.js in the public folder and add the following code to it:

const fileInput = document.getElementById('fileInput');
const uploadForm = document.getElementById('uploadForm');
const fileList = document.getElementById('fileList');
const uploadBtn = document.getElementById('uploadBtn');
const results = document.getElementById('results');

document.addEventListener('DOMContentLoaded', function() {
    
    let selectedFiles = [];
    
    // Handle file selection
    fileInput.addEventListener('change', function(e) {
        selectedFiles = Array.from(e.target.files);
        displaySelectedFiles(selectedFiles);
        uploadBtn.disabled = selectedFiles.length === 0;
    });
    
    // Handle form submission
    uploadForm.addEventListener('submit', function(e) {
        e.preventDefault();
        if (selectedFiles.length > 0) {
            uploadFiles(selectedFiles);
        }
    });
    
    // Display selected files
    function displaySelectedFiles(files) {
    fileList.innerHTML = '';

    files.forEach((file, index) => {
        const fileItem = document.createElement('div');
        fileItem.className = 'flex items-center justify-between bg-white border border-gray-200 rounded-lg shadow-sm p-3 mb-2';

        fileItem.innerHTML = `
            <div class="flex flex-col">
                <span class="file-name font-medium text-gray-800">${file.name}</span>
            </div>
            <button 
                type="button" 
                class="remove-file text-red-500 hover:text-red-700 font-bold text-lg px-3" 
                onclick="removeFile(${index})">
                ×
            </button>
        `;
        fileList.appendChild(fileItem);
    });
}

    
    // Remove individual file
    window.removeFile = function(index) {
        selectedFiles.splice(index, 1);
        displaySelectedFiles(selectedFiles);
        uploadBtn.disabled = selectedFiles.length === 0;
        
        // Update file input
        const dt = new DataTransfer();
        selectedFiles.forEach(file => dt.items.add(file));
        fileInput.files = dt.files;
    };
});

function uploadFiles(files) {
        const formData = new FormData();
        
        // Add each file to FormData
        files.forEach((file, index) => {
            formData.append('files', file);
            formData.append(`fileInfo_${index}`, JSON.stringify({
                originalName: file.name,
                size: file.size,
                type: file.type
            }));
        });
        
        sendUploadRequest(formData);
}
    
    
async function sendUploadRequest(formData) {
        try {
            const response = await fetch('/upload', {
                method: 'POST',
                body: formData
            });
    
            if (response.ok) {
                const result = await response.json();
                handleUploadSuccess(result);
            } else {
                handleUploadError(`Upload failed with status: ${response.message}`);
            }
        } catch (err) {
            handleUploadError(`Network error: ${err.message}`);
        }
}


function handleUploadSuccess(response) {
    results.classList.remove('hidden');
    results.innerHTML = `
        <div class="bg-green-50 border border-green-200 rounded-lg p-4 shadow-sm">
            <h3 class="text-lg font-semibold text-green-700 mb-2">Upload Results</h3>
            <ul class="space-y-2">
                ${response.files.map(file => `
                    <li class="flex justify-between items-center bg-white border border-gray-200 rounded-md px-3 py-2 shadow-sm">
                        <span class="text-gray-800 font-medium">${file.originalName}</span>
                        <span class="text-sm text-green-600">
                            ${file.status}
                        </span>
                    </li>
                `).join('')}
            </ul>
        </div>
    `;
    
    // Reset form
    selectedFiles = [];
    fileList.innerHTML = '';
    uploadBtn.disabled = true;
    fileInput.value = '';
}

function handleUploadError(error) {
    results.classList.remove('hidden');
    results.innerHTML = `
        <div class="bg-red-50 border border-red-200 text-red-700 rounded-lg p-4 shadow-sm">
            <p class="font-semibold">Error:</p>
            <p class="mt-1 text-sm">${error}</p>
        </div>
    `;
}

There’s a lot going on in the above code, but let’s break it down.

  • When a user selects files, they are stored in selectedFiles, displayed in a styled list (fileList), and the upload button is enabled.
  • window.removeFile allows users to remove individual files from the list, which also updates the underlying fileInput.files.
  • On form submit, the uploadFiles() is called. Inside it, the selected files are wrapped in a FormData object along with their metadata like name, size, type and sent to the /upload server using the Fetch API (handled by the sendUploadRequest function).
  • If the upload is successful, the results are displayed in a styled success box, the selected files list is cleared, the input is reset, and the upload button is disabled again (handled by handleUploadSuccess); if it fails, an error message is shown (handled by handleUploadError).

Now you should be able to select multiple images as shown in the image below:

If you click ‘Upload Files’, you will notice it doesn’t have any effects – this is because we haven’t written the code to handle the file upload on our Express server. Let’s do that now.

Handle File Upload on the Server with Multer

In a real-world application, you’d typically want to upload your users’ files to a Blob database such as Amazon S3, for example. However, to avoid the complexities of setting up a database, we’ll configure Multer to upload the files to disk temporarily.

Using Multer requires setting up a few things. We need to configure where to upload the files temporarily; we’ll use Multer’s DiskStorage for this. We’ll also use the fileFilter function to control which files should be uploaded or skipped. For instance, we could use this function to set a size upload limit of 10 MB.

Open the server.js file and modify its content to the following:

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

const app = express();
const PORT = process.env.PORT || 3000;

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

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

// File filter function

const upload = multer({
    storage: storage,
    limits: {
        fileSize: 10 * 1024 * 1024, // 10MB per file
    }
});

// Serve static files
app.use(express.static(path.join(__dirname, 'public')));

// Handle multiple file upload
app.post('/upload', upload.array('files', 10), (req, res) => {
    try {
        if (!req.files || req.files.length === 0) {
            return res.status(400).json({
                success: false,
                message: 'No files uploaded'
            });
        }
        
        // Process uploaded files
        const fileResults = req.files.map(file => ({
            originalName: file.originalname,
            filename: file.filename,
            size: file.size,
            mimetype: file.mimetype,
            path: file.path,
            status: 'Uploaded successfully'
        }));
        
        res.json({
            success: true,
            message: `${req.files.length} files uploaded successfully`,
            files: fileResults
        });
        
    } catch (error) {
        console.error('Upload error:', error);
        res.status(500).json({
            success: false,
            message: 'Upload failed',
            error: error.message
        });
    }
});

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

Now you can upload multiple images to the server using the HTML form:

And you can see the files uploaded to disk:

And there you have it – you have successfully implemented multiple file uploads in JavaScript!

But in a real-world application, you wouldn’t want to save files on disk. You need a database for that. However, setting up a database to handle file uploads can be complex. Instead of writing extensive code to handle file uploads, you can leverage a platform that automates much of the work for you.

In the next section, we’ll show how Cloudinary can save you from the complexities involved in setting up a database, making your file upload workflow more simple.

Simplifying Media Uploads With Cloudinary

Cloudinary is a cloud-based media management platform that makes it easy to store, transform, and deliver images, videos, and other files. Instead of writing complex logic for file storage and optimization, Cloudinary automates it for you. Some of the benefits Cloudinary provide include:

  • Automatic media storage in the cloud.
  • Powerful APIs for uploads, transformations, and delivery.
  • Built-in CDN for fast global delivery.
  • Scalability without managing servers or storage directly.

To start using Cloudinary, you’ll need to sign up for a free Cloudinary account to get your product environment credentials (cloud name, API key, and API secret). In the next steps, we’ll cover how to integrate Cloudinary in the previous example.

Step 1: Install Project Dependencies

In the project root, run the command below to install the required packages:

npm install cloudinary dotenv
  • cloudinary: The official Node.js SDK for interacting with Cloudinary’s APIs, enabling file uploads, transformations, and media management.
  • dotenv: A package for managing environment variables in Node.js applications.

Step 2: Configure Environment Variables

Create a .env file in the project root and replace the placeholder text with your Cloudinary credentials:

CLOUDINARY_CLOUD_NAME=your_cloud_name
CLOUDINARY_API_KEY=your_api_key
CLOUDINARY_API_SECRET=your_api_secret

Step 3: Modify server.js to use Cloudinary

Open the server.js file and replace its content with the following:

const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const cloudinary = require('cloudinary').v2;
require('dotenv').config();

const app = express();
const PORT = process.env.PORT || 3000;

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

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

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

// File filter function

const upload = multer({
    storage: storage,
    limits: {
        fileSize: 10 * 1024 * 1024,
    }
});

// Serve static files
app.use(express.static(path.join(__dirname, 'public')));

// Function to delete temporary file
const deleteLocalFile = (filePath) => {
    if (fs.existsSync(filePath)) {
        fs.unlinkSync(filePath);
    }
};

const uploadToCloudinary = async (filePath) => {
    try {
        const result = await cloudinary.uploader.upload(filePath, {
            resource_type: 'auto',
            folder: 'uploads',
            use_filename: true,
            unique_filename: true,
        });
        console.log('Cloudinary upload result:', result);
        return result;
    } catch (error) {
        throw new Error(`Cloudinary upload failed: ${error.message}`);
    }
}

// Handle multiple file upload
app.post('/upload', upload.array('files', 10), async (req, res) => {
    try {
        if (!req.files || req.files.length === 0) {
            return res.status(400).json({
                success: false,
                message: 'No files uploaded'
            });
        }
        // Upload all files to Cloudinary
        const uploadPromises = req.files.map(async (file) => {
            try {
                // Upload to Cloudinary
                const cloudinaryResult = await uploadToCloudinary(file.path);
                
                // Delete temporary local file
                deleteLocalFile(file.path);
                
                return {
                    originalName: file.originalname,
                    cloudinaryUrl: cloudinaryResult.secure_url,
                    status: 'SUCCESS'
                };
            } catch (uploadError) {
                // Delete temporary file even if upload fails
                deleteLocalFile(file.path);
                
                return {
                    originalName: file.originalname,
                    status: 'FAILED',
                    error: uploadError.message
                };
            }
        });

        // Wait for all uploads to complete
        const fileResults = await Promise.all(uploadPromises);
        
        // Ssuccessful and failed uploads
        const successfulUploads = fileResults.filter(result => result.status === 'SUCCESS');
        const failedUploads = fileResults.filter(result => result.status === 'FAILED');
        
        // Return response if the upload was successful
        res.json({
            message: `${successfulUploads.length} of ${req.files.length} files uploaded successfully`,
            successful: successfulUploads,
            failed: failedUploads
        });
        
    } catch (error) {
        console.error('Upload error:', error);
        
        // Clean up any temporary files in case of error
        if (req.files) {
            req.files.forEach(file => {
                deleteLocalFile(file.path);
            });
        }
        // Return error response if the upload failed
        res.status(500).json({
            success: false,
            message: 'Upload failed',
            error: error.message
        });
    }
});

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

In the above code, there are a few things going on:

  • The cloudinary.config() call initializes the Cloudinary SDK with your credentials (cloud_name, api_key, and api_secret) pulled from your .env file. This authenticates the application so it can upload files to your Cloudinary account.
  • Next, the uploadToCloudinary(filePath) function takes the files stored temporarily on disk by Multer and uploads them to Cloudinary using cloudinary.uploader.upload().
  • Once the files have been uploaded to Cloudinary, the corresponding temporary local files are removed with deleteLocalFile().
  • Finally, if the upload is successful, we extract the secure_url (a publicly accessible link to the uploaded file) of each file from Cloudinary’s response and send it back to the client in the JSON response.

And with this simple setup, you’ve successfully implemented a robust file upload workflow using Multer for handling incoming files and Cloudinary for cloud-based storage and delivery!

This approach not only offloads the heavy lifting of file storage from your server but also gives you access to Cloudinary’s powerful CDN, optimization, and transformation features. You can easily extend this workflow to handle other asset types (like videos or PDFs) and even apply transformations on the fly.

Wrapping Up

Multiple file upload functionality is no longer a luxury but a necessity in modern web applications. Throughout this guide, we’ve covered the essential foundations you need for implementing file uploads in JavaScript applications.

While this post covers the basics, you can learn more about advanced file upload techniques such as file preview, drag and drop, file scanning for virus and malwares, content moderation, and more, to provide a better user experience and ensure the integrity of your own services.

Frequently Asked Questions

Can I implement file upload without a server?

You can handle files client-side (e.g., reading them with FileReader), but to store them permanently, you need a server or a cloud service like Cloudinary.

Can I use drag-and-drop functionality with multiple files upload?

Yes you can, using the HTML Drag and Drop API. Basically, you’ll add dragover and drop event listeners to a div, when users drag files over it, you prevent the default behavior and highlight the drop area. When files are dropped, you capture them via event.dataTransfer.files and then process them like regular input files.

Can I upload different file types in the same request?

Yes, you can upload different file types together. For example, you can do this:

// ...
// Process different types differently
app.post('/upload', upload.array('files'), (req, res) => {
    const images = req.files.filter(file => file.mimetype.startsWith('image/'));
    const documents = req.files.filter(file => file.mimetype.includes('pdf') || file.mimetype.includes('document'));
    
    // Process images (resize, transform, save on Cloudinary)
    // Process documents (extract text, generate thumbnails, save to S3 bucket)
});
QUICK TIPS
Colby Fayock
Cloudinary Logo Colby Fayock

In my experience, here are tips that can help you better master JavaScript-based multiple file uploads beyond what’s covered in the article:

  1. Deduplicate files before upload
    Implement a file hash check (e.g., MD5 or SHA-1) in the browser using the FileReader API to avoid uploading duplicates, especially useful in image or document-heavy applications.
  2. Throttle or batch uploads for large files
    Rather than uploading all files in one go, consider using throttling or batching (e.g., 2–3 files per request) to manage memory usage and avoid overwhelming your backend or Cloudinary rate limits.
  3. Use optimistic UI updates
    Show upload progress immediately with optimistic feedback (e.g., placeholders or loading bars) even before the server responds. This improves perceived speed and user satisfaction.
  4. Encrypt files client-side before upload
    For applications with sensitive data (legal, health, finance), add client-side encryption (e.g., AES via Web Crypto API) before uploading. Decrypt server-side as needed for compliance.
  5. Implement resumable uploads
    For large files or poor network conditions, use a resumable upload strategy (e.g., tus.io or Uppy with tus) to ensure files don’t restart from scratch if interrupted.
  6. Validate file types using MIME signature sniffing
    Don’t rely solely on accept or mimetype—use FileReader and ArrayBuffer to validate the file’s binary signature (magic numbers), ensuring malicious files aren’t spoofed as images.
  7. Track upload history with persistent metadata
    Store metadata like upload time, client device info, and original path in a log or DB for auditing, debugging, or rollback functionality.
  8. Add server-side image processing fallbacks
    While Cloudinary handles transformations well, add backup logic server-side (e.g., using Sharp in Node.js) to resize/compress images if the cloud service is unavailable.
  9. Support progressive upload UX on slow networks
    Show file thumbnails progressively as uploads complete, not after all files are uploaded—this works well with Cloudinary’s fast CDN and auto-format conversion.
  10. Integrate file scanning and moderation as async jobs
    Offload malware scanning or content moderation (e.g., NSFW detection, profanity check) to asynchronous services (e.g., AWS Lambda + ClamAV or third-party APIs) triggered post-upload for scalability.
Last updated: Oct 12, 2025