How to: Active Storage with a Rails API and JavaScript Frontend, with Amazon S3

Jenny Kim
9 min readOct 10, 2021

--

For those looking to configure Active Storage with a Rails API as your backend and Javascript as your frontend, fear not — you’ve come to the right place!

After scouring the internet, looking over the Active Storage documentation numerous times, and getting lost and confused over the many different methods, I had finally figured out how to get my project and code working. Without Active Storage, my back-up plan was prompting users to copy and paste a URL of the picture they wanted to upload, but why do that when we have Active Storage! 🤩📦

To give you a quick background on my project and why I wanted/needed Active Storage: I created a picture board SPA (single-page application), similar to VSCO/Tumblr, where users could create “collections,” or albums, and upload photos onto said collections. Overall, my project used PostgreSQL for the database (to later make Heroku deployment easier), Ruby on Rails for the backend, JavaScript for the frontend, Active Storage to attach the file uploads to their Active Record objects, and Amazon S3 as my cloud storage (personally to make showing my project to others easier). Configuring a cloud storage service is optional. You can use local storage in the development stage, but since I used Amazon S3 for both the development and production environment, I’ll be discussing in this article how to set that up as well.

Backend Setup

Diagram of model associations and flow chart

As you can see, the idea for my backend was kept fairly simple. A user has many collections, which has many pictures, and for each picture object, it has an Active Storage file attached to it.

Now, let’s get started on the actual set up! First, to create your Active Storage database tables, run:

rails active_storage:install

Afterwards, you’ll need to migrate your tables:

rails db:migrate

I personally like to migrate my tables all at once, after I’ve generated my models, so I went ahead and created my user, collection, and picture models, as well as set up their has_many and belongs_to relationships.

Next, we’re going to set up our Active Storage services. Go to your config/storage.yml file and uncomment out test, local, and Amazon.

This is what your storage.yml file should look like, except your region and bucket information for Amazon will be different; however, let’s not concern ourselves with S3 just yet.

Then, you’ll need to tell Active Storage which services you’re using for which environment. So, go to your config/environments folder and for each file, locate this line of code:

config.active_storage.service

In each of your environment file, that line of code above should look like the screenshot from the Active Storage documentation below.

I’ve personally set my service in my development.rb file to Amazon’s, instead of local, and while we’re talking about Amazon, let’s quickly talk about how to get S3 set up.

Amazon S3

  1. Install this gem in your gemfile and run bundle in your terminal.
gem "aws-sdk-s3", require: false
  1. If you haven’t already, create and activate your AWS account.
  2. Grab your S3 credentials, which you’ll need for your access key and secret access key. You may want to download the CSV file onto your computer.
  3. Create your S3 bucket. The name should pertain to your project and must be unique.
  4. Now, for the exciting part! We’ll be encrypting our access keys. Run the following code in your terminal:
rails credentials:edit

This generic piece of code will automatically set the EDITOR environment value to your default editor, but if nothing happens, you should explicitly state your editor. For example, for your editor of choice, you should run:

// VS Code
EDITOR="code --wait" rails credentials:edit
// Atom
EDITOR="atom --wait" rails credentials:edit
// Sublime Text
EDITOR="sublime --wait" rails credentials:edit

The --wait simply tells the terminal to wait till you’re done inputting your credentials and have closed the file.

Upon running either one of the codes above, a temporary file will open. Uncomment out the following lines and copy and paste your access keys from Amazon.

aws:
access_key_id: 12345
secret_access_key: 12345

Once you close the file, it’ll be encrypted and Rails will automatically create a config/credentials.yml.enc file. While we don’t need to go into further detail regarding this file, the following information is very important! Make sure your config/master.key file, which should’ve been created upon your Rails app creation, is listed in your .gitignore file. The master key should NOT be uploaded to Github.

5. Finally, head back to your config/storage.yml file and edit your region and bucket information. Now when your application runs, everything should be connected!

In case you want to check that your access_key_idand secret_access_keyis indeed pointing to the right values, you can run the following codes in your terminal:

Rails.application.credentials.dig(:aws, :access_key_id)Rails.application.credentials.dig(:aws, :secret_access_key)

Now let’s get back to our backend setup!

Back to Active Storage on the backend

The way Active Storage files attach to their records is through has_one_attached and has_many_attached. For my project, I chose to opt for each picture having just one Active Storage file attached to it. Essentially, a picture is attached to a picture object.

Picture Model

Starting with my Picture model, I’ve created a custom method called pic_url to handle grabbing the attached picture’s URL from Active Storage. This method should be added as an attribute to your serializer (if you chose to use one), so your application can access this URL for the picture src.

Serializer using Active Model for my backend data

Trying to figure out how to get the URL of the Active Storage attachment was a doozy. I was initially following the documentation that suggested using url_for, but for my particular project needs this was pointing me towards the wrong direction. In retrospect it seemed as though the answer was right in front of me, but I did wish the documentation had explicitly stated which methods were available for us to use with the Active Storage attachment. Therefore, I suggest you check out all the different methods available by running Picture.img_file.methods and Picture.img_file.attachment.methods in your console for your respective models and Active Storage file(s).

Once you’ve added the URL attribute to your serializer, you can boot up rails s and go to localhost:3000. If you’ve set up your models and their relationships correctly (as well as seeded data), you should be able to see the URL for each object!

Introducing FormData

Next, I’ll be discussing FormData which held the key to making file uploads from my frontend properly save to my Rails API in the back. 🗝

In our Pictures controller, don’t forget to include yourimg_file , or whatever you’ve named the attachment file, into the strict params in your controller. Here, I’ve named my strict params method picture_params and you may note it looks a bit different.

Picture Controller

Normally, my strict params would include require() and look like this:

params.require(:picture).permit(:collection_id, :img_file)

Normally, we’d send our form data to our backend as an object with key value pairs, then send it through to our POST request with fetch() as the body by doing JSON.stringify(). However, because we’re sending a file, we need to utilize FormData and send it through as a FormData object. Files can’t be converted to strings and therefore can’t be formatted for JSON. This requires our strict params to be set up a bit differently, like in the above screenshot of my Pictures controller.

For those who aren’t familiar with FormData, it is not iterable and the data inside FormData can only be accessed a certain way. If you’re debugging and console.logging, only to find that your FormData object is “empty,” don’t panic as your code may have worked — you’re simply accessing the data wrong. I highly suggest you take a look at MDN’s documentation on FormData to check out the different methods to access the data.

With this, let’s move onto our frontend setup, where I’ll continue to talk about FormData more in depth.

Frontend Setup

I needed a way for my picture upload form to be created dynamically for each collection upon click, so that the this keyword was pointing to the correct collection object. Therefore, I created a method (createAddBtn()) to create a new picture form each time a collection was accessed.

Picture upload form (left), in the form of a “plus” button

The code you should be primarily looking at are lines 123–130. I’ve extracted the important pieces of code from those lines down below:

<input type="file" name="img_file" multiple/>

The multiple will allow users to upload multiple files at once. Simply remove this if you only want users to upload one file.

The name of this input element should point to what key you want in your strong params. Since I’ve named my attachment file in my params img_file, I’ve done the same with my input element.

The next important piece of information you should know about FormData is that you have to access the files from the input element through .files. If there are multiple files, you can access them by iterating through them or calling .files[i]. Take a look at the screenshot down below to get an example:

I’ve selected the input element from the DOM on line 135 and added an event listener to it. My “form” only consisted of an upload button, which is why I personally set myaddEventListener to fire on change instead of submit. Since users can upload multiple pictures at once for my application, I’m iterating through input.files on line 138.

Then I created a new FormData object on line 139 and appended the key value pairs afterwards, ultimately submitting the newly created formData object to a POST request with fetch on the last line, through the uploadPictures() method. To read up on creating FormData objects more in depth, you should follow this guide on MDN.

And now that we’ve sent our formData object to our uploadPictures() method, let’s talk about the difference between sending a FormData object and a regular object to a POST request with fetch.

The difference between the two POST requests: Pictures using FormData (left), Collections using JSON (right)

The left screenshotPOSTs a FormData object and the right a regular Collections object with JSON. As you can see, the body of our configObj on the left is our formData object from the previous input form, where we created said formData object on line 139. Comparatively, we are using JSON.stringify() for our body of our configObj on the right. You may also note that the left screenshot excludes headers.

Once again, this is because our collection object, created on line 21 (on the right screenshot), only contains strings and therefore can be formatted for JSON, whereas FormData objects cannot.

With that, congratulations! 🥳 You’ve reached the end of setting up Active Storage for a Rails API as a backend and JavaScript for frontend, and if you’ve followed the instructions for setting up Amazon S3, you’ve got that accomplished as well!

My biggest takeaway from this project is that I shouldn’t solely rely on documentations to help me out with obstacles that pertained specifically to my project. I thought that since documentations are the official standards, I’d only need to follow these instructions to get my program to work. Ultimately, I’d had been chasing the wrong rabbit with trying to get my application to work with Direct Upload, which I much later realized was not what I was looking for for my project, since I didn’t want my picture uploads to bypass my backend server and directly save onto the cloud.

What’s important to realize is that since your project will undoubtedly be/look different from mine, you may need to program things a bit differently and perhaps in different orders, but for the most part, I hope this article helps you save all the hours I will never get back. 🥲

--

--

Jenny Kim

I’m a recent graduate from Carnegie Mellon University with a business degree, currently attending Flatiron School for Software Engineering!