How to Upload Files with JavaScript

In this post, we use JavaScript to create the file upload request. You'll learn how to access files, create the HTTP request, and make it reusable.

Welcome back to this series, all about uploading files to the web. If you miss the first post, I’d recommend you check it out because it’s all about uploading files via HTML.

  1. Upload files with HTML
  2. Upload files with JavaScript
  3. Receive uploads in Node.js (Nuxt.js)
  4. Optimize storage costs with Object Storage
  5. Optimize performance with a CDN
  6. Upload security & malware protection

In this post, we’ll do the same thing using JavaScript.

Set up an event handler

We left the project off with the form that looks like this.

<form action="/api" method="post" enctype="multipart/form-data">
  <label for="file">File</label>
  <input id="file" name="file" type="file" />
  <button>Upload</button>
</form>

In the previous post, we learned that in order to access a file on the user’s device, we had to use an <input> with the “file” type. And in order to create the HTTP request to upload the file, we had to use a <form> element.

When dealing with JavaScript, the first part is still true. We still need the file input to access the files on the device. However, browsers have a Fetch API that we can use to make HTTP requests without forms.

I still like to include a form because:

  1. Progressive enhancement: If JavaScript fails for whatever reason, the HTML form will still work.
  2. I’m lazy: The form will actually make my work easier later on, as we’ll see.

With that in mind, for JavaScript to submit this form, I’ll set up a “submit” event handler.

const form = document.querySelector('form');
form.addEventListener('submit', handleSubmit);

/** @param {Event} event */
function handleSubmit(event) {
  // The rest of the logic will go here.
}

Throughout the rest of this post, we’ll only be looking at the logic within the event handler function, handleSubmit.

Prepare the HTTP request

The first thing I need to do in this submit handler is call the event’s preventDefault method to stop the browser from reloading the page to submit the form. I like to put this at the end of the event handler so that if there is an exception thrown within the body of this function, preventDefault will not be called, and the browser will fall back to the default behavior.

/** @param {Event} event */
function handleSubmit(event) {
  // Any JS that could fail goes here
  event.preventDefault();
}

Next, we’ll want to construct the HTTP request using the Fetch API. The Fetch API expects the first argument to be a URL, and a second, optional argument as an Object.

We can get the URL from the form’s action property. It’s available on any form DOM node which we can access using the event’s currentTarget property. If the action is not defined in the HTML, it will default to the browser’s current URL.

/** @param {Event} event */
function handleSubmit(event) {
  const form = event.currentTarget;
  const url = new URL(form.action);

  fetch(url);

  event.preventDefault();
}

Relying on the HTML to define the URL makes it more declarative, keeps our event handler reusable, and our JavaScript bundles smaller. It also maintains functionality if the JavaScript fails.

By default, Fetch sends HTTP requests using the GET method, but to upload a file, we need to use a POST method. We can change the method using fetch‘s optional second argument. I’ll create a variable for that object and assign the method property, but once again, I’ll grab the value from the form’s method attribute in the HTML.

const url = new URL(form.action);

/** @type {Parameters<fetch>[1]} */
const fetchOptions = {
  method: form.method,
};

fetch(url, fetchOptions);

Now the only missing piece is actually including the payload in the body of the request.

Add the request body

If you’ve ever created a Fetch request in the past, you may have included the body as a JSON string or a URLSearchParams object. Unfortunately, neither of those will work to send a file, as they don’t have access to the binary file contents.

Fortunately, there is the FormData browser API. We can use it to construct the request body from the form DOM node. And conveniently, when we do so, it even sets the request’s Content-Type header to multipart/form-data; also a necessary step to transmit the binary data.

const url = new URL(form.action);
const formData = new FormData(form);

/** @type {Parameters<fetch>[1]} */
const fetchOptions = {
  method: form.method,
  body: formData,
};

fetch(url, fetchOptions);

That’s really the bare minimum needed to upload files with JavaScript. Let’s do a little recap:

  1. Access to the file system using a file type input.
  2. Construct an HTTP request using the Fetch (or XMLHttpRequest) API.
  3. Set the request method to POST.
  4. Include the file in the request body.
  5. Set the HTTP Content-Type header to multipart/form-data.

Today we looked at a convenient way of doing that, using an HTML form element with a submit event handler, and using a FormData object in the body of the request. The current handleSumit function should look like this.

/** @param {Event} event */
function handleSubmit(event) {
  const url = new URL(form.action);
  const formData = new FormData(form);

  /** @type {Parameters<fetch>[1]} */
  const fetchOptions = {
    method: form.method,
    body: formData,
  };

  fetch(url, fetchOptions);

  event.preventDefault();
}

Unfortunately, the current submit handler is not very reusable. Every request will include a body set to a FormData object and a “Content-Type” header set to multipart/form-data. This is too brittle. Bodies are not allowed in GET requests, and we may want to support different content types in other POST requests.

Make it reusable

We can make our code more robust to handle GET and POST requests, and send the appropriate Content-Type header. We’ll do so by creating a URLSearchParams object in addition to the FormData, and running some logic based on whether the request method should be POST or GET. I’ll try to lay out the logic below:

  • Is the request using a POST method?
    • Yes: is the form’s enctype attribute multipart/form-data?
      • Yes: set the body of the request to the FormData object. The browser will automatically set the “Content-Type” header to multipart/form-data.
      • No: set the body of the request to the URLSearchParams object. The browser will automatically set the “Content-Type” header to application/x-www-form-urlencoded.
    • No: We can assume it’s a GET request. Modify the URL to include the data as query string parameters.

The refactored solution looks like:

/** @param {Event} event */
function handleSubmit(event) {
  /** @type {HTMLFormElement} */
  const form = event.currentTarget;
  const url = new URL(form.action);
  const formData = new FormData(form);
  const searchParams = new URLSearchParams(formData);

  /** @type {Parameters<fetch>[1]} */
  const fetchOptions = {
    method: form.method,
  };

  if (form.method.toLowerCase() === 'post') {
    if (form.enctype === 'multipart/form-data') {
      fetchOptions.body = formData;
    } else {
      fetchOptions.body = searchParams;
    }
  } else {
    url.search = searchParams;
  }

  fetch(url, fetchOptions);

  event.preventDefault();
}

I really like this solution for a number of reasons:

  • It can be used for any form.
  • It relies on the underlying HTML as the declarative source of configuration.
  • The HTTP request behaves the same as with an HTML form. This follows the principle of progressive enhancement, so file upload works the same when JavaScript is working properly or when it fails.

So, that’s it. That’s uploading files with JavaScript.

I hope you found this useful and plan to stick around for the whole series. In the next post, we’ll move to the back end to see what we need to do to receive files.

  1. Upload files with HTML
  2. Upload files with JavaScript
  3. Receive uploads in Node.js (Nuxt.js)
  4. Optimize storage costs with Object Storage
  5. Optimize performance with a CDN
  6. Upload security & malware protection

Thank you so much for reading. If you liked this article, and want to support me, the best ways to do so are to share it, sign up for my newsletter, and follow me on Twitter.


Originally published on austingil.com.

13 Comments

  1. Rookie calling… Do you have an “upload.html” working example which uploads ‘file’ from ‘source’ to ‘target’? PLEASE !

    • Hey Andreas, thanks for reaching out. That’s a good question, but I think I need more context for what you’re trying to do. What do you mean by ‘file’, ‘source’, and ‘target’? In the browser, the file would generally be a file you get access from using `<input type=”file”/>`. The user must interact with it and select the file from their computer. There’s no other way to access the file system through the browser. As from ‘source’ and ‘target’, I really don’t know what you mean. The source could be either the webpage or the user’s computer. And the ‘target’ would need to be some web server that is capable of receiving files. It’s not covered in this post, but the next post in the series covers that.

      Feel free to reach out with more questions or details.

      • Thanks for your time to reply! My JS manages documents. When the user adds a new document my JS must upload that {file}from the user’s “disk”, the {source}, into one of the “public-html/Docs/xxx/” folders, the {target}. My script knows the {file} and {target} names. The user supplies the {source} directory name. Could you please publish or email an “upload.html”, a fully working example which uploads {file} from {source} to {target}?

  2. Hi, Austin,

    I’m really a JS novice and I’m trying unsuccessfully to get this to work in conjunction with an HTML webform.

    First, when I load the page that contains the added JavaScript, I get this console error:

    Uncaught TypeError: Cannot read properties of null (reading ‘addEventListener’)
    at need-help-test:162:7

    Then, when I submit the form, the receiving website (which I don’t manage) returns a 500 error.

    Any suggestions are welcome!

    Thanks,
    Eddie

    • Hey Eddie, thanks for reaching out! It would be helpful to see your file, need-help-test at line 162. Based on your error message, that’s where I would look first.

      Generally speaking, that error suggests you’re calling addEventListener on something that doesn’t exist. The only place I mention it above is right after accessing a form DOM node using querySelector. In truth, it’s possible that the returned value from querySelector could be null, if there is no form on the page. It may also be possible if your JavaScript executes BEFORE the form is on the page.

      As for the 500 error from the server, this might be something you can’t do anything about. A server will determine whether they accept data submitted at all, what type of data, which senders they allow, and much more. So while this post was intended to teach one side of the equation, it’s still necessary for the receiving end to allow for uploads.

      I’m happy to discuss in more detail if you like. Maybe we can set up a 30 min call :)

      • Hi, Austin,

        Thanks for the reply. I moved the JavaScript code after the form, but I still get the error (Uncaught TypeError: Cannot read properties of null (reading ‘addEventListener’)) and I still get the 500 error at the receiving website. I know the receiving end allows file uploads because we’ve tested it elsewhere.

        Here is the form where I’m trying to get this to work:

        https://mps.rutgers.edu/need-help-test

        For an example of where this is working, see the “Need Help?” widget at the lower corner of: https://mps.rutgers.edu/data-science/

        I didn’t write the code for the widget, but I have been tasked with reproducing its functionality on a web page.

        Thanks again,
        Eddie

        • Eddie, the first problem is because you are passing “widget_form” to querySelector. QuerySelector uses any valid CSS style selector, so what you are telling it is to return the first HTML widget_form tag it finds. But that is not a valid tag. It looks like you used “widget_form” as the form’s ID, so you can either use document.getElementById(“widget_form”) or pass the CSS syntax for referencing an ID document.querySelector(“#widget_form”)

          See: https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector

          The form issue would be much more involved to sort out this way, but the first thing I notice is you set the form’s action to “https://project.sas.rutgers.edu/requests/helpdesk_widget/create_ticket”, while the help widget submits to “https://project.sas.rutgers.edu/requests/helpdesk_widget/create_ticket.js” (notice the .js file extension at the end)

          • Austin,

            Thanks again for your help! I am making progress, but am not quite there yet. I changed the first line to:

            const form = document.getElementById(“widget_form”);

            This eliminated the console error upon page load. And when I submit the form with no attachment, redirection to the proper location is now working.

            However, the file attachment problem persists. But it seems to be a CORS issue. The error received is:

            Access to fetch at ‘https://project.sas.rutgers.edu/requests/helpdesk_widget/create_ticket’ from origin ‘https://mps.rutgers.edu’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. If an opaque response serves your needs, set the request’s mode to ‘no-cors’ to fetch the resource with CORS disabled.
            need-help:430

            POST https://project.sas.rutgers.edu/requests/helpdesk_widget/create_ticket net::ERR_FAILED 500 (Internal Server Error)
            handleSubmit @ need-help:430
            need-help:430

            Uncaught (in promise) TypeError: Failed to fetch
            at HTMLFormElement.handleSubmit (need-help:430:3)
            handleSubmit @ need-help:430

            Best regards,
            Eddie

  3. Thanks for your time to reply! My JS manages documents. When the user adds a new document my JS must upload that {file}from the use’s “disk”, the {source}, into one of the “public-html/Docs/xxx/” folders, the {target}. My script knows the {file} and {target} names. The user supplies the {source} directory name. Could you please publish or email an “upload.html”, a fully working example which uploads {file} from {source} to {target}?

    • I’m sorry, but I’m still a little unclear about what you are trying to accomplish. But I should mention that client-side JS can’t determine where a file will be saved. It can send that information along with the request, but what happen on the server side, where it gets saved, or whatever is going to be up to the server-side logic.

Leave a Reply

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