How to: Active Storage with a Rails API and JavaScript Frontend, with Amazon S3
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
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
- Install this gem in your
gemfile
and runbundle
in your terminal.
gem "aws-sdk-s3", require: false
- If you haven’t already, create and activate your AWS account.
- 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.
- Create your S3 bucket. The name should pertain to your project and must be unique.
- 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_id
and secret_access_key
is 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.
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
.
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.
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.log
ging, 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.
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 left screenshotPOST
s 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. 🥲