How to upload a file on the frontend and send it using JS to a Rails backend
By: Amy Resnik
Tags: Ruby on Rails, File Upload, Multipart Form Data, JavaScript, Json

How to upload a file on the frontend and send it using JS to a Rails backend Amy Resnik Feb 6, 2020·5 min read

I was recently working on a take home assessment where I had to let the user upload a file and then have the backend parse and manipulate the file. Doesn’t sound too bad, but when I sat down to do it I realized that I had never worked with uploading a file.

Cue a Google search where I learned that along with the input types I am used to like text , submit , password , etc., there is also an input type of file in an HTML form. For anyone curious, there are actually a ton of input types that I had never seen before that can be found here.

The input type of file allows user to choose one or more files from their computer. There are 4 additional attributes available when the type of file is used:

accept : describes file types to allow as denoted by unique file type specifiers

: describes file types to allow as denoted by unique file type specifiers capture : source to use if capturing image or video data

: source to use if capturing image or video data files : the list of selected files

: the list of selected files multiple : boolean, if true allows the user to select more than one file

For my assessment, I needed to let the user upload a single json file. So I created an HTML form with 2 inputs, one for the file and one for the submit button. Let’s break down the file input:

type="file" : allows the user to choose one or more files from their computer

: allows the user to choose one or more files from their computer name="uploadFile" : names the input field so I can later target it via JS

: names the input field so I can later target it via JS accept=".json" : only allows json files to be selected

: only allows files to be selected required : does not allow the user to submit the form without having a value in this input field

<form class="upload">

<input type="file" name="uploadFile" accept=".json" required />

<br/><br/>

<input type="submit" />

</form>

Visual representation of the above code to make the HTML form

Upon click of “Choose File”, I am prompted to select a file from my computer. On my desktop I currently have 2 screenshots and one test_data.json file. Since I specified that my input should only accept json files, I am not able to select either of screenshots (they are greyed out).

Prompt upon click of “Choose File”

Then using JavaScript, I added a submit event listener onto the form. Like any form, I can grab the input from the name attribute I gave it in the HTML, which in our case is e.target.uploadFile . Usually I would use e.target.uploadFile.value to get the value of the input, but for a file I actually want e.target.uploadFile.files , which is an array of the uploaded files. In this case only one file was uploaded so I can just grab the one at the zero index.

const uploadForm = document.querySelector('.upload') uploadForm.addEventListener('submit', function(e) {

e.preventDefault()

let file = e.target.uploadFile.files[0]

})

Now that I have the uploaded file, I want to send it to my Rails backend via a POST request, but this is where things get interesting. Normally a POST request looks something like this where I’m sending a JSON object as the body and including headers :

fetch('http://localhost:3000/users', {

method: "POST",

headers: {

"Content-Type": "application/json",

"Accept": "application/json"

},

body: JSON.stringify({

name: name,

})

})

.then(resp => resp.json())

.then(data => {

// do something here

})

But in this case, I don’t want to send a JSON object with values, I want to send the contents of the uploaded file. I’ll save you the frustration of trying lots of combinations of things that do not work. The solutions involves using FormData .

FormData is an object that lets you set key/value pairs to be sent in a request. It is usually used to send form data, hence the name. To utilize FormData you have to create a new instance of it, and then can use append to add key/value pairs. Here I am adding a key of file and setting it to the file variable that contains our uploaded file:

const uploadForm = document.querySelector('.upload') uploadForm.addEventListener('submit', function(e) {

e.preventDefault()

let file = e.target.uploadFile.files[0] let formData = new FormData()

formData.append('file', file)

})

Now I can finish my POST request and pass formData as the body . I also have my rails server set up locally on port 3000 and my route is /upload_files . Since we are using FormData , the content type is actually 'multipart/form-data' . However, it is important to not include headers: { "Content-Type": 'multipart/form-data' } in the POST request. The browser will set the content type for us, including another boundary parameter, and including the content type will actually cause major problems. A more detailed explanation can be found here.

const uploadForm = document.querySelector('.upload') uploadForm.addEventListener('submit', function(e) {

e.preventDefault()

let file = e.target.uploadFile.files[0] let formData = new FormData()

formData.append('file', file) fetch('http://localhost:3000/upload_files', {

method: 'POST',

body: formData

})

.then(resp => resp.json())

.then(data => {

if (data.errors) {

alert(data.errors)

}

else {

console.log(data)

}

})

})

Finally, to the Rails backend. In my UploadFiles controller, I have a create action where I ultimately want to read and parse the file, and do anything else I need with it. If I stick a byebug in that method, I can see what my params look like:

<ActionController::Parameters {"file"=>#<ActionDispatch::Http::UploadedFile:0x00007fc53f1be638 @tempfile=#<Tempfile:/var/folders/sl/nh9hb7j102q9xcwsy3jrtskw0000gn/T/RackMultipart20200206-63800-1mm9z70.json>, @original_filename="test_data.json", @content_type="application/json", @headers="Content-Disposition: form-data; name=\"file\"; filename=\"test_data.json\"\r

Content-Type: application/json\r

">, "controller"=>"upload_files", "action"=>"create"} permitted: false>

See that file within params ? It should look familiar since I added a key of file to my FormData .

So now I can look at params[:file] :

#<ActionDispatch::Http::UploadedFile:0x00007fc53f1be638 @tempfile=#<Tempfile:/var/folders/sl/nh9hb7j102q9xcwsy3jrtskw0000gn/T/RackMultipart20200206-63800-1mm9z70.json>, @original_filename="test_data.json", @content_type="application/json", @headers="Content-Disposition: form-data; name=\"file\"; filename=\"test_data.json\"\r

Content-Type: application/json\r

">

What I care about is the tempfile attribute which is a temporary copy of my file. I can call the read method on the temporary file, params[:file].tempfile , to get the text within the file. Since my file is JSON , I can then parse the contents of the file as JSON via JSON.parse :

def create

begin

file = params[:file].tempfile.read

data = JSON.parse(file)

render json: data

rescue

render json: { errors: 'Upload failed' }

end

end

I’m using a begin/rescue here so that if I run into any errors when reading or parsing, I can send back a generic “Upload failed” error message. I’m also just sending back the read and parsed data (which is ultimately just getting logged to the console), but you can really do anything with this data once you have it parsed.

Hope this was helpful!

Sources

https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file

https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#Unique_file_type_specifiers