Due to the availability of third-party packages, we don’t worry enough about how exactly a JavaScript file uploader works. If you have basic knowledge of HTML, CSS, and JavaScript, it would be easy for you to build a JavaScript file uploader.
In this post, we’ll build a JavaScript file uploader with vanilla JavaScript from scratch. The goal is to build a JavaScript file uploader with external libraries to understand some of the JavaScript core concepts.
Let’s explore the steps that we’ll follow to build a JavaScript file uploader.
1: How Can We Set Up The Node.js Server To Build A JavaScript File Uploader?
To set up the backend server, we will use the beautiful, inbuilt HTTP package.
To begin with, we’ll create a new folder for the project.
mkdir fileupload-service
After that we’ll create an index.js file, defining the entry point of our backend server.
touch index.js
Now, let’s create the HTTP server.
const http = require('http'); // import http module
const server = http.createServer(); // create server
server.listen(8080, () => {
console.log('Server running on port 8080') // listening on the port
})
Done! The above code is pretty self-explanatory to build a JavaScript file uploader.
Finally, we have created an HTTP server running on port 8080.
2: How Can We Set Up The Front End?
First, to build a JavaScript file uploader we’ll create a basic HTML file to set up the front end. It will come with file input and an upload button. It will initiate the uploading process when clicked. Lastly, there would be a tiny status text that would declare the file upload status.
In vanilla JS, attach an event listener to add an action on any button click.
Code Example
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>File Uploader</title>
</head>
<body>
<h2>File Upload Service</h2>
<input type="file" id="file">
<button id="upload">Upload</button>
<small id="status"></small>
<script>
const file = document.getElementById('file');
const upload = document.getElementById('upload');
const status = document.getElementById('status');
upload.addEventListener('click', () => {
console.log('clicked the upload button!');
})
</script>
</body>
</html>
Users can select the file and easily upload it by clicking the upload button.
We must send this file from the backend to serve this HTML file on calling the home route. The most straightforward approach can be like this:
server.on('request', (req, res) => {
if(req.url === '/' && req.method === 'GET') {
return res.end(fs.readFileSync(__dirname + '/index.html'))
}
})
Here is the server.on(‘request’) method is used to listen to all HTTP requests in a Node backend server.
3: How Can We Read The File Content On The Front End?
Now that our backend server is up and running, we will need to read the file on the front end. For this, we can use the FileReader object. Because it lets web applications asynchronously read the contents of files stored on the user’s computer using File or Blob objects to specify the file or data to read.
The syntax is:
const fileReader = new FileReader(); // initialize the object
fileReader.readAsArrayBuffer(file); // read file as array buffer
We can access selected input files under the files field for the input. We are only building it for a single file upload.
const selectFile = file.files[0];
To read a file, FileReader provides a couple of methods:
- FileReader.readerAsBinaryBuffer()– read file as array buffer
- File Reader.readerAsBinaryString– read the file in raw binary data
- FileReader.readAsDataURL()– read the file and returns the result as a data URL
- FileReader.readAsText()– if we are aware of the type of file as text, this method is useful
Here, we’ll use the readAsArrayBuffer-method to read the file in bytes and stream it to the backend over the network.
A couple of event listeners – like onload or onprogress – are provided by FileReader to track reading the file on the client-side.
Once the file is thoroughly read, we split it into small chunks and stream it to the backend.
Code Example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>File Uploader</title>
</head>
<body>
<h2>File Upload Service</h2>
<input type="file" id="file">
<button id="upload">Upload</button>
<small id="status"></small>
<script>
const file = document.getElementById('file');
const upload = document.getElementById('upload');
const status = document.getElementById(status);
upload.addEventListener('click', () => {
// set status to uploading
status.innerHTML = ‘uploading…’;
const fileReader = new FileReader();
fileReader.readAsArrayBuffer(file.files[0]);
fileReader.onload = (event) => {
console.log('Complete File read successfully!')
}
});
</script>
</body>
</html>
The cause behind using a <small> tag that changes to uploading until finished is that it starts with uploading, and once the file is stored on the backend successfully, it shows the message uploaded.
4: How To Divide And Stream The File In Chunks To The Backend?
If the file size is large, it’s not good practice to send the complete file at once, because some proxy servers like Nginx might block it. After all, it seems malicious. It would be better if we split this file into chunk sizes of ~5000 bytes and sent it to the backend.
To hit the backend server, fetch is here to the rescue.
We’ll use the event parameter. Once it has read the file, we can access the file’s content as an array buffer in the event. Target: Result field.
First, we need to split the array buffer of this file into chunks of 5000 bytes. Then make it orderly before sending backend to avoid corrupt file messages.
Code Example
// file content
const content = event.target.result;
// fix chunk size
const CHUNK_SIZE = 5000;
// total chunks
const totalChunks = event.target.result.byteLength / CHUNK_SIZE;
// loop over each chunk
for (let chunk = 0; chunk < totalChunks + 1; chunk++) {
// prepare the chunk
let CHUNK = content.slice(chunk * CHUNK_SIZE, (chunk + 1) * CHUNK_SIZE)
// todo - send it to the backend
}
Next, use async await while uploading to avoid flooding the backend server with requests.
fileReader.onload = async (event) => {
const content = event.target.result;
const CHUNK_SIZE = 1000;
const totalChunks = event.target.result.byteLength / CHUNK_SIZE;
// generate a file name
const fileName = Math.random().toString(36).slice(-6) + file.files[0].name;
for (let chunk = 0; chunk < totalChunks + 1; chunk++) {
let CHUNK = content.slice(chunk * CHUNK_SIZE, (chunk + 1) * CHUNK_SIZE)
await fetch('/upload?fileName=' + fileName, {
'method' : 'POST',
'headers' : {
'content-type' : "application/octet-stream",
'content-length' : CHUNK.length,
},
'body': CHUNK
})
}
status.innerHTML = ‘uploaded!!!’;
You may have noticed that we have added the file name as a query parameter. It can make you wonder why we are also sending the file name. Notice that all the API calls to the backend server are stateless, so, to append the content to a file, we need to have a unique identifier, which would be our case name.
We need a unique identifier to make sure the backend works as expected.
So, we use this:
Math.random().toString(36).slice(-6)
Practically, we should not send any custom header because most proxies, like Nginx or HAProxy, might block it.
5: How To Receive The Chunks And Store Them On The Server?
Until now, we have completed the frontend setup. The next step is to listen to the file chunks and write them to the server.
To extract the file name from the query params of the request, we use the code below:
const query = new URLSearchParams(req.url);
const fileName = query.get(‘/upload?fileName’);
Code Example
So, the final code will look like this:
server.on('request', (req, res) => {
if(req.url === '/' && req.method == 'GET') {
return res.end(fs.readFileSync(__dirname + '/index.html'))
}
if(req.url=== '/upload' && req.method == 'POST') {
const query = new URLSearchParams(req.url);
const fileName = query.get(‘/upload?fileName’);
req.on('data', chunk => {
fs.appendFileSync(fileName, chunk); // append to a file on the disk
})
return res.end('Yay! File is uploaded.')
}
})
6: How Can We Upload Multiple Files In A JavaScript File Uploader?
So far, we have built a beautiful single file upload application with vanilla JS. The next goal is to extend our current implementation to support multiple file uploads.
Check that the backend is smart enough to work for multiple file uploads smoothly. Because it has a straightforward job, take a chunk and append it to the respective file name received in the request. It is independent of how many files are uploaded from the front end.
Therefore, let’s take advantage of it and improve our application.
Firstly, modify the file input to accept multiple file selections on the UI. By default, it tends to accept a single file. We use the multiple options in input to allow multiple files.
<input type="file" id="files" multiple>
We are all set to accept multiple files in the file input.
Entire input files are now accessible via the files.files array. So, our thought is pretty simple: we will iterate over the array of selected files, break it into chunks, stream it to the backend server, and store it there.
for(let fileIndex=0;
fileIndex<files.files.length;fileIndex++) { const file = files.files[fileIndex]; // divide the file into chunks and upload it to the backend }
Our good friend for loop makes it very simple to go over each file and upload it to the backend. To keep track of file upload status, we maintain a variable that gets updated on each file upload.
Code Example
Our file upload script looks like this:
const files = document.getElementById('files');
const upload = document.getElementById('upload');
const status = document.getElementById('status');
upload.addEventListener('click', () => {
// set loading status
status.innerHTML = 'uploading...';
let fileUploaded = 0;
for(let fileIndex = 0; fileIndex < files.files.length; fileIndex++) {
const file = files.files[fileIndex];
const fileReader = new FileReader();
fileReader.readAsArrayBuffer(file);
fileReader.onload = async (event) => {
const content = event.target.result;
const CHUNK_SIZE = 1000;
const totalChunks = event.target.result.byteLength / CHUNK_SIZE;
const fileName = Math.random().toString(36).slice(-6) + file.name;
for (let chunk = 0; chunk < totalChunks + 1; chunk++) {
let CHUNK = content.slice(chunk * CHUNK_SIZE, (chunk + 1) * CHUNK_SIZE)
await fetch('/upload?fileName=' + fileName, {
'method' : 'POST',
'headers' : {
'content-type' : "application/octet-stream",
'content-length' : CHUNK.length
},
'body' : CHUNK
})
}
fileUploaded += 1;
status.innerHTML = `file ${fileUploaded} of ${files.files.length} uploaded!!!`;
}
}
})
We are not waiting for the previous file to upload completely, as the files are being uploaded in parallel. Our backend is stateless, so it functions accordingly.
That’s all! You can now build your own JavaScript file uploader.
But building a JavaScript file uploader is not all. First, it is risky to deal independently with scaling, security, and maintenance. Instead, you can use Filestack to scale quickly and painlessly in addition to minimizing risks. Moreover, Filestack has built-in protection and time-saving maintenance features. So, you can focus on developing rather than maintaining, scaling, and securing your applications.
Are You Ready To Build Your Own JavaScript File Uploader Using Filestack?
Filestack is the best developer service for uploads. Get user content from anywhere and improve file or video uploads with powerful, easy-to-use Filestack. It makes API uploads, URL ingestion, and iOS/Android device integration fast and easy.
What are you waiting for! Head over to Filestack and sign up for free today!
Filestack is a dynamic team dedicated to revolutionizing file uploads and management for web and mobile applications. Our user-friendly API seamlessly integrates with major cloud services, offering developers a reliable and efficient file handling experience.
Read More →