File Uploads for the Web (2): Upload Files with JavaScript
Tags: webdev, javascript, programming, tutorial
Published: 2023-03-28 14:01:24

Welcome back to this series, all about uploading files to the web. If you miss [the first post](https://austingil.com/uploading-files-with-html/), I’d recommend you check it out because it’s all about uploading files via [HTML](https://austingil.com/category/html/).

1. [**Upload files with HTML**](https://austingil.com/uploading-files-with-html/)
    
2. [**Upload files with JavaScript**](https://austingil.com/uploading-files-with-javascript/)
    
3. **Receiving file uploads with Node.js (Nuxt.js)**
    
4. **Optimizing storage costs with Object Storage**
    
5. **Optimizing delivery with a CDN**
    
6. **Securing file uploads with malware scans**
    

In this post, we’ll do the same thing using [JavaScript](https://austingil.com/category/javascript/).

{% embed https://www.youtube.com/watch?v=Zyjgc2bySZo %}

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

```xml
<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`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file). And in order to create the [HTTP request](https://developer.mozilla.org/en-US/docs/Web/HTTP) to upload the file, we had to use a [`<form>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/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](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) that we can use to make HTTP requests without forms.

I still like to include a form because:

1. [**Progressive enhancement**](https://austingil.com/resilient-applications-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](https://developer.mozilla.org/en-US/docs/Web/Events/Event_handlers).

```javascript
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`.

So the first thing I need to do in this submit handler is call the event’s [`preventDefault`](https://developer.mozilla.org/en-US/docs/Web/API/Event/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](https://developer.mozilla.org/en-US/docs/Glossary/Exception) thrown within the body of this function, `preventDefault` will **not** be called, and the browser will fall back to the default behavior.

```javascript
/** @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](https://developer.mozilla.org/en-US/docs/Web/API/fetch#resource), and a second, [optional argument as an Object](https://developer.mozilla.org/en-US/docs/Web/API/fetch#options).

We can get the URL from the form’s [`action`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/action) property. It’s available on any [form DOM node](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement) which we can access using the event’s [`currentTarget`](https://developer.mozilla.org/en-US/docs/Web/API/Event/currentTarget) property. If the `action` is not defined in the HTML, it will default to the browser’s current URL.

```javascript
/** @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`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/GET) method, but to upload a file, we need to use a [`POST`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/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`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/method) attribute in the HTML.

```javascript
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.

If you’ve ever created a Fetch request in the past, you may have included the body as a [JSON string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) or a [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/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`](https://developer.mozilla.org/en-US/docs/Web/API/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`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) header to `multipart/form-data`; also a necessary step to transmit the binary data.

```javascript
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`](https://developer.mozilla.org/en-US/docs/Web/API/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.

```javascript
/** @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.

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`](https://developer.mozilla.org/en-US/docs/Web/API/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`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/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:

```javascript
/** @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**](https://austingil.com/uploading-files-with-html/)
    
2. [**Upload files with JavaScript**](https://austingil.com/uploading-files-with-javascript/)
    
3. **Receiving file uploads with Node.js (Nuxt.js)**
    
4. **Optimizing storage costs with Object Storage**
    
5. **Optimizing delivery with a CDN**
    
6. **Securing file uploads with malware scans**
    

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](https://twitter.com/share?via=heyAustinGil), [sign up for my newsletter](https://austingil.com/newsletter/), and [follow me on Twitter](https://twitter.com/heyAustinGil).

---

*Originally published on* [*austingil.com*](http://austingil.com)*.*