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:
- The basics of uploading multiple files using JavaScript
- How to set up a Node.js backend to process file uploads
- How to use Cloudinary as a file upload storage
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 aFormData
object along with their metadata like name, size, type and sent to the/upload
server using the Fetch API (handled by thesendUploadRequest
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 byhandleUploadError
).
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
, andapi_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 usingcloudinary.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) });