Uploading photos in JavaScript web applications involves both client-side and server-side processes. On the client side, developers typically use the <input type="file"> element (optionally with accept="image/*" and multiple attributes) to let users select one or more image files. JavaScript’s File API and FileReader enable previewing images before upload by converting them to base64-encoded URLs.
For sending images to a server, the FormData object is commonly used, allowing files to be transmitted via fetch or XMLHttpRequest. On the backend, a dedicated endpoint receives the uploaded files, processes them, and stores them locally or in cloud storage, returning a response with upload status or file URLs.
This tutorial demonstrates these concepts by building a simple image uploader with JavaScript, covering file input, image preview, drag-and-drop, and the basics of sending images to a server.
Join thousands of businesses transforming their digital asset management with Cloudinary. Sign up for free today!
How Do You Upload an Image to a Server With JavaScript?
To upload an image to a server with JavaScript, put the file from an <input type=”file”> into a FormData object and POST it with fetch, then accept the multipart/form-data request on the server with a parser such as multer and write the file to storage. The browser sets the Content-Type header along with the boundary string the server needs in order to split the request body back into fields.
Here is the whole job in two files. The markup:
<input type="file" id="fileUpload" accept="image/*"> <p id="status"></p>
The browser code, in index.js:
const fileInput = document.getElementById("fileUpload");
const status = document.getElementById("status");
fileInput.addEventListener("change", async () => {
const [file] = fileInput.files;
if (!file) return;
const formData = new FormData();
formData.append("image", file);
status.textContent = "Uploading...";
try {
// No headers object: the browser sets the content type and the boundary.
const response = await fetch("/upload", { method: "POST", body: formData });
if (!response.ok) {
throw new Error(`Upload failed with status ${response.status}`);
}
const data = await response.json();
status.textContent = `Uploaded to ${data.url}`;
} catch (error) {
status.textContent = error.message;
}
});
And the server, using Express and multer:
import express from "express";
import multer from "multer";
const app = express();
const upload = multer({ dest: "uploads/" });
app.post("/upload", upload.single("image"), (request, response) => {
if (!request.file) {
return response.status(400).json({ error: "No file uploaded" });
}
response.json({ url: `/uploads/${request.file.filename}` });
});
app.use("/uploads", express.static("uploads"));
app.listen(3000);
- The string in
formData.append("image", file)and the string inupload.single("image")are the same field name. If they differ, the request arrives with no file and request.file is undefined. upload.single()handles one file. For several, useupload.array("images", 10)and readrequest.files.- The server returns a URL, but nothing serves that URL until you add app.use(“/uploads”, express.static(“uploads”)).
The rest of this article builds that out: previewing the file in the browser first, drag and drop, validation on the server, a progress bar, and the option of skipping your own endpoint.
Uploading an Image Using The File API
The File API is a Web API that enables web applications to access files and their contents. Developers can implement this feature in two ways:
- An
inputelement with atype="file"to let the user choose one or more files from their device storage. - Using the HTML Drag and Drop API to allow users to upload files to a web application.
In the steps to follow, we’ll explain how to implement a simple image uploader in HTML, CSS, and JavaScript.
Step 1: HTML and CSS Structure
First, let’s create the structure of what the web page we’ll use in this example will look like. Create a file named index.html and add the following code to it:
Next, create another file named index.css and add the following code to it:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
flex-direction: column;
background-color: white;
font-family: Arial, sans-serif;
}
.container {
display: flex;
justify-content: center;
align-items: center;
border: 2px solid #ccc;
padding: 20px;
margin-bottom: 30px;
border-radius: 10px;
background-color: #f9f9f9;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
#fileUpload {
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
cursor: pointer;
}
Here’s what’s going on in the two blocks of code above.
- The HTML code defines the structure of the page.
- The HTML also includes a link to an external
index.cssfor styling and anindex.jsfile which will handle the functionality for image uploading and displaying, which we will create in a moment.
Here’s what the result looks like in the browser:
Step 2: Handle Image Upload Using JavaScript
In the same folder with the previous HTML and CSS files, create another file named index.js and add the following code to it:
const fileInput = document.getElementById("fileUpload")
const imageOutput = document.getElementById("output");
fileInput.addEventListener("change", async () => {
let [file] = fileInput.files
const reader = new FileReader();
reader.onload = (e) => {
imageOutput.src = e.target.result;
};
reader.onerror = (err) => {
console.error("Error reading file:", err);
alert("An error occurred while reading the file.");
};
reader.readAsDataURL(file);
})
Let’s break down what’s going on in that code block:
- The first part of the code selects the input and
imgelements using their respective IDs. - Next, we added an event listener to the
fileInputelement to listen for the “change” event, which triggers when a user selects a file. - Finally, when a file is selected, a
FileReaderobject reads the file as a Data URL. Once the file is read, the img elementsrcattribute is set to the resulting Data URL, displaying the selected image.
Specify Acceptable File Types for Upload
We can specify the acceptable file types the input element should accept using the accept attribute. The value is a comma-separated list of allowed file extensions or MIME types. For example, accept="image/png, image/jpeg" or accept=".png, .jpg, .jpeg" accept PNG or JPEG files. We can also use a wildcard to accept any type of image file extension, as shown below:
<input type="file" id="fileUpload" accept="image/*">
Accepting multiple files
By default, a user can only upload a single file. However, we can use the multiple Boolean attribute to allow users to upload more than one file at a time.
<input type="file" id="fileUpload" multiple>
Previewing Multiple Images Before Upload
When allowing multiple file uploads, you can preview all selected images before uploading them. Use JavaScript’s File API and FileReader to loop through the selected files and display each image in the browser:
<input type="file" id="imageUpload" accept="image/*" multiple>
<div id="imagePreview"></div>
const imageUpload = document.getElementById('imageUpload');
const imagePreview = document.getElementById('imagePreview');
imageUpload.addEventListener('change', (event) => {
const files = event.target.files;
imagePreview.innerHTML = ''; // Clear previous previews
for (const file of files) {
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (e) => {
const img = document.createElement('img');
img.src = e.target.result;
img.style.maxWidth = '200px'; // Example styling
imagePreview.appendChild(img);
};
reader.readAsDataURL(file);
}
}
});
Note that the block above holds two different things: the first two lines are HTML and the rest is JavaScript. Put the markup in index.html and the script in index.js, or wrap the script in a <script> tag. Pasted into a single HTML file as one block, it does nothing.
To send all of the selected files rather than only preview them, append each one under the same field name, then read request.files on the server after upload.array(“images”, 10):
async function uploadAll() {
const formData = new FormData();
for (const file of imageUpload.files) {
formData.append("images", file);
}
const response = await fetch("/upload-many", {
method: "POST",
body: formData
});
if (!response.ok) {
throw new Error(`Server returned ${response.status}`);
}
const { urls } = await response.json();
return urls;
}
Upload an Image Using Drag and Drop
The HTML drag-and-drop interface allows web apps to implement drag-and-drop through the following two steps:
- Specify a target element for the file drop, also known as the “drop zone”.
- Specify event handlers for the “drop” and “dragover” events.
Let’s see how to do this in the series of steps below.
Step 1: HTML and CSS Structure
Open the index.html file and replace its contents with the following:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Drag and drop image upload</title>
<link rel="stylesheet" href="index.css">
</head>
<body>
<div class="container">
<div class="drag-drop-area" id="dragDropArea">
<p>Drag & Drop your files here</p>
</div>
<div id="output"></div>
</div>
<script src="script.js"></script>
</body>
</html>
Then, in the index.css file, replace the code with the following:
body {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: #f0f0f0;
}
.container {
text-align: center;
}
.drag-drop-area {
border: 2px dashed #ccc;
padding: 20px;
border-radius: 10px;
background-color: white;
cursor: pointer;
width: 300px;
margin: 20px auto;
position: relative;
}
.drag-drop-area p {
margin: 10px 0;
}
#fileInput {
display: none;
}
#output {
margin-top: 20px;
}
#output img {
max-width: 100%;
height: auto;
}
Here’s what the webpage looks like:
Step 2: Add JavaScript
In the index.js file, replace the code with the following:
const dragDropArea = document.getElementById("dragDropArea");
const output = document.getElementById("output");
// Step 1 - Add an event listener for the dragover event
dragDropArea.addEventListener("dragover", (e) => {
e.preventDefault(); // This turns off the browser's default drag and drop handler.
dragDropArea.classList.add("dragover");
});
// Step 2 - Add an event listener for the drop event
dragDropArea.addEventListener("drop", (e) => {
e.preventDefault();
dragDropArea.classList.remove("dragover");
const files = e.dataTransfer.files;
if (files.length === 0) {
alert("No files selected.");
return;
}
output.innerHTML = "";
for (const file of files) {
if (!file.type.startsWith("image/")) {
alert("Only image files are allowed.");
return;
}
const reader = new FileReader();
reader.onload = (e) => {
const img = document.createElement("img");
img.src = e.target.result;
output.appendChild(img);
};
reader.onerror = (err) => {
console.error("Error reading file:", err);
alert("An error occurred while reading the file.");
};
reader.readAsDataURL(file);
}
});
Again, let’s break down the logic behind the code:
- The first part of the code selects two HTML elements: the drag-drop area and an output area.
- Next, we create event handlers to handle the “drop” and “dragover” events.
- We also use the
DataTransferobject (ine.dataTransfer.files) to access the data being dragged during the drag-and-drop operation. - Then, we process the dropped files using the
FileReadermethod and create an img element to display the uploaded image file.
Two gaps are worth closing in the code above. The dragover handler adds the dragover class and only the drop handler removes it, so dragging a file onto the zone and then away again leaves the highlight switched on. Add a dragleave handler as well:
dragDropArea.addEventListener("dragleave", () => {
dragDropArea.classList.remove("dragover");
});
And the highlight needs a rule to match, which the stylesheet above does not yet contain, so classList.add(“dragover”) currently changes nothing on screen:
.drag-drop-area.dragover {
border-color: #3448c5;
background-color: #eef1ff;
}
Uploading Images to a Server Using JavaScript
After selecting and previewing images, you often need to upload them to a server for storage or further processing. JavaScript’s FormData object makes this easy by allowing you to append files and other form fields, then send them via fetch or XMLHttpRequest:
// Assuming 'files' contains the selected image files
const formData = new FormData();
for (const file of files) {
formData.append('images', file); // 'images' is the field name on the server
}
fetch('/upload-endpoint', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => console.log('Upload success:', data))
.catch(error => console.error('Upload error:', error));
Add one check to that call before shipping it. fetch rejects only on a network failure, so a 413 Payload Too Large, or a 500 that returns an HTML error page, is a resolved promise. Calling response.json() on it throws a parse error that tells you nothing about the real problem. Test response.ok first, then read the body:
fetch('/upload-endpoint', {
method: 'POST',
body: formData
})
.then(response => {
if (!response.ok) {
throw new Error(`Server returned ${response.status}`);
}
return response.json();
})
.then(data => console.log('Upload success:', data))
.catch(error => console.error('Upload error:', error));
On the server side, you’ll need an endpoint (for example, in Node.js, Python, or PHP) to receive the uploaded files, save them to disk or cloud storage, and return a response with the upload status or file URLs.
How Do You Receive an Image Upload on a Node.js Server?
The endpoint is the half of this job the browser cannot do for you. Here it is in Express, with multer as the multipart parser. Configure multer with a size limit, a type filter and generated filenames, because the filename the browser sends is chosen by whoever sends it, and ../../etc/passwd is a valid string:
import express from "express";
import multer from "multer";
import crypto from "node:crypto";
const app = express();
const EXTENSIONS = {
"image/jpeg": ".jpg",
"image/png": ".png",
"image/webp": ".webp",
"image/avif": ".avif"
};
const storage = multer.diskStorage({
destination: "uploads",
filename: (request, file, callback) => {
// Never reuse file.originalname: generate the name yourself.
callback(null, crypto.randomUUID() + EXTENSIONS[file.mimetype]);
}
});
const upload = multer({
storage,
limits: { fileSize: 5 * 1024 * 1024, files: 1 },
fileFilter: (request, file, callback) => {
if (!EXTENSIONS[file.mimetype]) {
return callback(new Error("UNSUPPORTED_TYPE"));
}
callback(null, true);
}
});
app.post("/upload", (request, response) => {
// Calling the middleware here, rather than as route middleware,
// keeps multer's own errors in this handler.
upload.single("image")(request, response, (error) => {
if (error instanceof multer.MulterError) {
const status = error.code === "LIMIT_FILE_SIZE" ? 413 : 400;
return response.status(status).json({ error: error.code });
}
if (error) {
return response.status(400).json({ error: error.message });
}
if (!request.file) {
return response.status(400).json({ error: "No file uploaded" });
}
response.json({
url: `/uploads/${request.file.filename}`,
bytes: request.file.size
});
});
});
app.use("/uploads", express.static("uploads"));
app.listen(3000, () => console.log("Listening on http://localhost:3000"));
Three notes on that code:
destinationis passed as a string here, and multer creates the directory for you. Multer makes you responsible for creating it when you pass destination as a function instead. The shorter version used earlier also works, but it stores each file under a random name without an extension.- Calling
upload.single("image")inside the handler, rather than as route middleware, is multer’s documented way to catch its own errors. Left as route middleware, an oversized file becomes a generic Express error page instead of a 413 your client can read. - The paths here are relative, so they resolve against the directory you start the process from. Use an absolute path if the server is started from somewhere else.
How Do You Validate an Image Upload Securely?
An upload endpoint accepts bytes from strangers, which makes it the most exposed route in most applications. The checks in the browser are a courtesy: they save the user from waiting out an upload you were going to reject anyway, and an attacker skips them by posting to your endpoint directly. Four checks on the server are the ones that count.
- Do not trust the declared type. The file.mimetype multer gives you comes from the Content-Type of the multipart part, which the client writes, and file.type in the browser is a guess from the file extension. Renaming payload.exe to cat.png changes both. Read the first bytes of the stored file and compare them against the format’s signature:
- Set the limit in two places. Multer’s limits.fileSize stops the write, but the bytes still travel. Cap the request body at the reverse proxy as well, with client_max_body_size in nginx, so an oversized POST is refused before it reaches Node.
- Generate every filename. The code above uses crypto.randomUUID(). Reusing file.originalname invites path traversal, collisions that overwrite another user’s file, and names that break on a different filesystem.
- Serve uploads from somewhere that cannot execute them. A directory handed to express.static will return an HTML file that a user uploaded, and HTML served from your own origin runs scripts with your origin’s privileges. Object storage, or a CDN on a separate hostname, removes that class of problem.
Wrapping Up
In this article, we explored two methods of uploading image files in a web application using HTML, CSS, and JavaScript: the traditional file input approach and the interactive drag-and-drop method. While several libraries simplify the process, knowing how to write the code on your own will allow you to gain deeper insights into browser APIs and improve your overall web app development skills.
The server half is the part worth getting right. A FormData object and fetch move the file out of the browser, a multipart parser such as multer turns the request back into a file, and the field name in formData.append() has to match the one in upload.single() or the file never arrives at all. The checks that matter, the size, the real type read from the file’s leading bytes, and a filename you generate yourself, belong on the server, because the client-side versions are a convenience an attacker skips. After that, the open question is whether to keep running storage and delivery yourself, or hand them to a service and keep your endpoint for the logic only you can write.
Empower your development team with Cloudinary’s easy-to-use APIs and SDKs. Sign up for free today!
Frequently Asked Questions
Should you send a base64 data URL instead of the file?
No, unless an API asks for one. Base64 encodes three bytes as four characters, so the payload is around a third larger for no benefit. FileReader.readAsDataURL() is for previewing in the browser, and FormData sends the raw bytes.
What is the largest image you can upload?
Whatever the smallest limit in the chain allows, and there are usually three: your reverse proxy, your parser and your host. In the code above, multer’s limits.fileSize sets 5 MB. Through Cloudinary’s API, files larger than 100 MB must be sent in chunks, and the total depends on your plan.
Is the accept attribute enough validation?
No. accept filters the file picker dialog, and file.type is the browser’s guess from the extension. A request that never opens a dialog bypasses both. Check the size and the leading bytes of the file on the server.