meta pixel

Upload files directly to AWS S3 in a Rails/React App


    Posted by Tolulope Olusakin

    on Apr 29th 2025


Since my last article, I’ve built two full-stack Rails/React applications, one of which is a hobby/passion project, ShowJunkie (demo login: guest@showjunkie.com, password: ShowJunkiePerson), and another is my personal portfolio (the subject of today’s discussion). So I decided to be adventurous and venture into rewriting my personal portfolio, and in a bid to indulge my curious self a bit more, I decided I wanted to upload my project images directly to AWS and store the link in my Postgres database for reference in my client-side React app. Well, I succeeded — but not without high-fiving my forehead a million times and questioning my skills as a developer numerous times. In an attempt to save anyone else from reliving my last two days, I decided to write this guide. Without further ado, I give you the one guide you’ll need to: Set up an AWS bucket Generate a presigned URL to use for your upload with your Rails API and send it to your React app Upload an image to AWS with your React app Store the image link in your database so you can reference it in your app later on So why? Why upload directly to your AWS S3 bucket instead of first to your Rails application and then off to the AWS S3 bucket? The answer is your users. It can save a lot of time and directly improve the experience your users have with your application. Think about it, if you were to take the latter route of first uploading to your Rails app before shipping off to the bucket, first, you’d give your servers double the required amount of work and your users double the amount of time they have to wait — not to mention the very possibility an upload could fail midway and require a restart. How awful. The solution? An AWS S3 bucket that acts as a go-between. What’s this bucket we’ve been talking about? Think of it as a folder living on the web. It can have numerous subfolders and hold an infinite amount of wisdom or, rather, files. So let’s get started, shall we? First Steps The first logical step will be to head over to AWS, create an account (not to worry — they offer a free tier for 12 months you can use to practice), and sign into the console. The signup form looks like the one below:

After your signup, log into the console with your credentials, and you’ll find yourself somewhere like the screen below:

Click the services drop-down in the navigation, and in the search box, type S3 like so:

Select the S3 option at the top, and it’ll take you to the S3 management console, where we’ll configure your very own S3 bucket. As you can see in the image below, I have the one for my Rails API very much alive.

Create a bucket, and follow the prompt. In the “Configure options” pane, you don’t have to check any box — just move on to “Set permissions.” Here, uncheck the “all public access” box (you can turn it on once you’re sure you need it or your application will function right with it). Move on to the preview pane, and create the bucket. It wasn’t so bad, right?

What creating a bucket looks like Step 2 Generate a policy for your bucket by clicking on your bucket in the list. Click the permissions tab, scroll to the bottom, and hit the policy generator link — which should take you here:

Remember to select the S3 bucket policy in the “Select Type of Policy” drop-down. Your principal will take the following format: arn:aws:iam::${YOURACCOUNTID_HERE}:root. You can find your account ID at the top of the “My Account” section in your AWS console under account settings. Your ARN can be found where you clicked the policy generator beside “Bucket policy editor” here:

Finish up the policy by selecting GetObject and PutObject in the actions drop-down. Then, click “Generate Policy,” copy the policy, and paste it in the window in the image above. Add /* to the end of the resource value (it references your entire bucket directory — remember a bucket is a fancy name for the folder in the sky), or you’ll get thes error Action does not apply to any resource(s) in statement when trying to save the policy. Save the policy, and you can move on. The policy should look like this:

Generated policy of example S3 Bucket Step 3 Move over to the “Access Control List” (ACL) tab, and under public access, enable the options like in the image below. Again, you can always change these permissions when you’re sure your app will function perfectly with/without them when you’re done. Move over to the “CORS configuration” tab, and paste in the CORS configuration I’ve entered below in this image:

Enabling public access to your bucket via ACLs

CORS Configuration Be sure to replace www.yourdomain.com in the second CORSRule with your own URL, or delete that CORSRule if yours doesn’t need it. Before I go any further, please note the http://and https:// were intentional, and no, it’s not my OCD. I’m detail-oriented, but it isn’t that — this implementation working hinges on their presence. I found out the hard way. Step 4 In your navigation bar, click the username with your drop-down, and select “My Security Credentials” here. We’ll add a user and create policies for that user. This user's credentials will be used to sign your URL in a bit, so please follow closely. In the menu on the left of that page, select “Users,” create a user, and give the user programmatic access. Hit next, click the “Attach existing policies directly” tab, search for s3, and select “AmazonS3FullAccess.” Skip the optional tags, review, and create the user. The entire flow should look like this:

User credentials Copy the Access Key ID and Secret Access Key in the window above, and fasten your seat belts — the fun is about to begin. Step 5 Add the gem 'aws-sdk'~2 to your Gemfile in your Rails application, and run bundle in your terminal. With the credentials from our last step in the image above, create a .local_env.yml in your Rails App’s config directory, and add your credentials like in the image below. Note: Remember to add this file to your gitignore, or face the consequences!

.local_env.yml

.gitignore with /config/localenv.yml Next, we need to load localenv.yml in our Rails configuration so we can access the credentials to create our AWS configuration. Put the snippet below in your application.rb inside the API module and Application class.

Loading your local_env.yml to give you access to your credentials in your Rails app locally Next, if you have your app running on Heroku already, you may also want to set the credentials by running the following commands in your CLI (your command line/terminal).

Step 6 Next, create a file named aws.rb in /config/initializers, and add the following snippet to your file. Be sure to crosscheck your bucket’s region here.

Now, we have our Aws object ready to run. Your credentials are references via your ENV variable — as you can see in the snippet above. Step 7 Set up a controller for the endpoint you’ll query from your React app to get your signed URL. I called mine s3uploadscontroller.rb . Put the snippet below in the controller, and don’t forget to update your routes file.

Controller action to generate and return signed URL and get URL Essentially, this controller action does two things: generates a signed URL and puts together a geturl for with the same key so we can reference our images later. It returns a JSON object with both URLs to our client app via a custom method I wrote, jsonresponse, which can be seen in the snippet below. The randompath is to take care of the off chance that I upload two files with the same name sometime in the near future. Filename, filetype, and directory are all parameters we’ll send from the React app. The presignedurl method takes an action, key, bucket name, ACL, and content type. Remember the actions and ACL settings from our AWS account? Winter is here. Please take care of the acl and content_type— they’re paramount to this implementation working perfectly. You may write tests for this if you’re a TDD junkie like I am, and if you aren’t, I suggest you take a critical look at your development process and the concept of TDD — it’ll literally save your life

Custom json_response method Step 8 We’ll now query our endpoint in our React app, get the signer URL, and upload a beautiful image to AWS S3 in our bucket. First, we need a file form element, if you don’t already have one. Next, we need a function to handle when the file’s selected and to possibly store it in our state. Next, we send the file off to the method handling the upload. I’ll show you all three snippets below.

Take note that we’re accessing the .files property on the element and not the value property and getting the first file via [0]. Concern yourself with the code in the if block alone. Next, the function we’ve all been waiting for!

So I’m using a Redux action to update and create my portfolio projects. Take a look at updateProkect for a second. Disregard auth_token — I’m passing in the form and the file and returning an async function that calls uploadToAWS if there’s a file to upload. Take note that I have a few custom objects and methods I wrote, like API and APIHelpers.projectImagePath(). API is my base Axios object, and the latter is a helper I wrote to consistently return the path for images in my app since I may be using it in numerous places. They’re subject to your own decisions and approach. See them below:

Inside of the uploadToAWS, I query the API endpoint I created in the Rails app at /upload. I also pass in the parameters for the file name, fileType, and directory, just as we set up the Rails controller to receive them. The headers aren’t mandatory — I’m using them to authenticate my user via JWT, but it may not be important to you if you don’t require auth. I go on to destructure the data I receive from the Rails app. Remember geturl and posturl? I wasn’t crazy. Also, take note of the content type and ACL in the options variable. Notice how they’re the exact same as in the Rails controller method? This is mandatory.

Finally, I use Axios to post the file with the options, I return the geturl to updateProject, and I send off the form to my Rails database with the URL as my imageurl, which I can use to display the image on my app.

Yep! We did it — whew! I hope this is helpful to someone and saves at least one person some hassle. I’d love to hear from anyone who has another approach to doing this or any suggestions/corrections for me! That being said, I wish you all the best with your implementations! Until next time, ciao!

Tolulope OlusakinTechnology Expert