MEDIA GUIDES / Front-End Development

Building an Image Upload Feature with JavaScript

js image uploading

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 in upload.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, use upload.array("images", 10) and read request.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.

js image uploading

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:

  1. An input element with a type="file" to let the user choose one or more files from their device storage.
  2. 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.css for styling and an index.js file 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:

js image uploading

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 img elements using their respective IDs.
  • Next, we added an event listener to the fileInput element to listen for the “change” event, which triggers when a user selects a file.
  • Finally, when a file is selected, a FileReader object reads the file as a Data URL. Once the file is read, the img element src attribute 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:

js image uploading

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 DataTransfer object (in e.dataTransfer.files) to access the data being dragged during the drag-and-drop operation.
  • Then, we process the dropped files using the FileReader method 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:

  • destination is 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.

QUICK TIPS
Jen Looper
Cloudinary Logo Jen Looper

In my experience, here are tips that can help you better implement and optimize an image upload feature with JavaScript:

  1. Validate file size before upload
    Implement client-side validation to check the file size before initiating the upload. This prevents users from uploading excessively large files, which can slow down the server and degrade user experience. You can set a maximum file size and display an error message if the file exceeds this limit.
  2. Compress images before upload
    Use JavaScript libraries like compress.js or pica to compress images client-side before they are uploaded. This reduces the file size without sacrificing too much quality, resulting in faster uploads and reduced server load.
  3. Use async/await for file processing
    When handling file uploads, especially if performing additional processing like compression or format conversion, use async/await to manage asynchronous operations. This makes your code more readable and easier to debug.
  4. Implement a progress indicator
    Provide users with feedback during the upload process by implementing a progress bar. This enhances the user experience by visually indicating how much of the file has been uploaded, especially for large files.
  5. Secure file uploads against XSS
    Always sanitize file names and validate file types server-side to prevent malicious files from being uploaded. Avoid relying solely on client-side validation, as it can be bypassed by attackers.
  6. Support multiple image formats
    Consider supporting modern image formats like WebP, AVIF, or JPEG XL in addition to traditional formats (JPEG, PNG). This ensures that your application can handle various image types efficiently, especially with regard to file size and quality.
  7. Implement drag-and-drop fallback
    For users with devices or browsers that do not fully support the drag-and-drop API, provide a fallback to the standard file input method. This ensures that all users can upload images, regardless of their environment.
  8. Optimize server-side image handling
    Once images are uploaded, optimize them server-side using tools like ImageMagick or Sharp. This ensures that images are properly resized, compressed, and converted to suitable formats before being stored or served.
  9. Enable chunked file uploads for large images
    For large images or poor network conditions, implement chunked file uploads. This technique splits a large file into smaller parts, uploads each part separately, and reassembles them server-side. This improves reliability and allows resuming uploads if interrupted.
  10. Implement image caching strategies
    Once images are uploaded and optimized, use a content delivery network (CDN) to cache and serve images efficiently. Implementing caching headers ensures that frequently accessed images are delivered quickly without repeatedly hitting your servers.
Last updated: Nov 4, 2026
★★★★★
4.5 (27 reviews)
Talk to Sales Talk to Sales