How To Upload Images to a Rails API — And Get Them Back Again

With a Rails API, image uploading isn’t as simple as it seems

Angus Morrison
Nov 13, 2019 · 7 min read
Photo by Alexander Andrews on Unsplash

I’d given myself a week to write a Rails API back end for Supagram, a lightweight, browser-based Instagram clone featuring posts, likes, and a chronological activity feed from the users you follow.

The biggest difficulty I foresaw was the polymorphic database relationships between users in their various roles as follower, followed, liker, and so on. Little did I know that the least intuitive thing to get working would actually be the simple concept around which all of Instagram is built: image uploading.

Let me walk you through the problem. This server was required to:

  • Accept an image file from the React front end
  • Associate the image with a newly created post record in the database
  • Upload the image to a content delivery network like Cloudinary or AWS for storage and retrieval
  • Fetch the image URL and return it as confirmation of the post’s creation and in subsequent GET requests to the activity feed

The existing resources on how to do this are extremely fragmented. Much had to be inferred or jury-rigged for the specific context of a Rails API. Here’s the definitive, step-by-step guide to save you the pain.


1. Accept an Image From a JavaScript Front End

There are two common ways to send image uploads to a server: as FormData, or as a base64 string. There are considerable performance disadvantages to base64 encoding and transmission, so I chose FormData.

I’ll be using React components to demonstrate, but FormData is a web API — it’s not constrained to any particular framework, or even JavaScript itself.

Here a simple HTML form takes a caption and an image to upload. The names of the fields should match the parameters your API endpoint is expecting to receive, in this case caption and image.

On submit, we prevent the default form behaviour (which refreshes the page) and use JavaScript’s FormData constructor to create a FormData object from event.target — the whole form.

That done, we make our first call to the API:

There are two important things to note about the config object for this request:

  • There is no "Content-Type" key in the headers — the content type is multipart/form-data, which is implied by the FormData object itself.
  • The body is not stringified. The FormData API handles all the necessary processing for the image to be sent over the web.

The authorization header is optional and will depend on the requirements of the endpoint you’re posting to. In the example, the endpoint I’m using is represented by POSTS_URL.


2. Associate the Image With a Newly-Created Post Record in the Database

On the back end, I used ActiveStorage to create associations between images and their owning objects. It’s the standard gem for file associations as of Rails 5.2 and is steadily replacing older solutions like CarrierWave and Paperclip.

To get started, simply run rails active_storage:install. It will create migrations for two new tables in your database, active_storage_blobs and active_storage_attachments. These are managed automatically; you don’t need to touch them. Run rails db:migrate to complete the process.

By default, ActiveStorage will use local storage for uploaded files while running in a development environment. In production, this is almost certainly not what you want. It also poses some unique challenges for returning image URLs from the server. We’ll configure this properly in part three, after we take a look at our Post model and accompanying endpoint.

Post migration/model

Study this migration for my post model. There’s something odd about it.

You’ll notice that there is nothing about an image here. Neither the image nor a reference to the image lives in the Posts table.

Now check out the model:

The essential line here is has_one_attached :image. This tells ActiveStorage to associate a file with a given instance of Post.

The name for the attached object should match the parameter being sent from the front end. I’ve called it :image, because that’s what I named the corresponding upload form field. You can call it whatever you like, so long as the front end and back end agree.

As a bonus, I’ve added validation to ensure that posts cannot be created without images. Change this to suit your purposes.

Curious about the include statement and my get_image_url method? Let’s inspect the post creation endpoint before coming back to these.

Post creation endpoint

The post_params method is arguably the most important here. The data from our front end has ended up in a Rails params hash with a body that looks roughly like: { "caption" => "Great caption", "image" => <FormData> }.

The keys of this hash must match the attributes expected by the model.

My particular post model requires a user_id, which wasn’t sent in the request body but is instead decoded from an Authorization token in the request headers. That’s happening behind the scenes in get_current_user(), and you don’t need to worry about it.

When you pass post_params() to Post.create(), ActiveStorage kicks in, saves a file based on the FormData contained within the image param, and associates the file with the new Post record. If you’re using local storage, images will be saved in root/storage by default. However, that’s probably not what you want.


3. Upload the Image to a CDN for Storage and Retrieval

Local storage eats up space and can’t compete with the delivery speeds of dedicated content delivery networks like Cloudinary and AWS. Whatever your purposes, it’s a good idea to familiarise yourself with these essential services.

Cloudinary is exceptionally user-friendly and easy to integrate with ActiveStorage, so that’s the approach I took for this project. From this point on, I’ll assume that you already have a (free) Cloudinary account. If you’d prefer to use another service, don’t worry — the approach is largely the same for all major providers.

First, add the cloudinary gem to your Gemfile and run bundle install.

Then, in /config, open up ActiveRecord’s storage.yml configuration file and add the following. Don’t modify anything else.

Next, navigate to config/environments/development.rb and ./production.rb, and set config.active_storage.service to :cloudinary in each. Your test environment will continue to use local storage by default.

Finally, download the cloudinary.yml config file from your Cloudinary dashboard and place it in the /config folder.

Find the YML download link in the top-right of your dashboard Account Details section.

Caution: This file contains the secret key for your Cloudinary account. Do not share this file or push it to your git repo, or your account can be compromised. Include /config/cloudinary.yml in your .gitignore file. If you do reveal these details by accident (I’m speaking from experience), immediately deactivate the compromised key and generate a new one via your Cloudinary dashboard. Update cloudinary.yml to reflect the new secret key.

With this in place, ActiveStorage will automatically upload and retrieve images from the cloud.


4. Fetch the Image URL and Return It

This is the least intuitive part of working with ActiveStorage. Getting images in is easy enough. Getting them out again without guidance is like solving a 12-sided Rubik’s cube drunk.

It becomes especially convoluted if, like me, you want to move the logic of building your endpoint’s response into a dedicated serializer class.

In my posts controller respond_to_post() method, I first check if the new post is valid and, if so, create an instance PostSerializer from the new post and the current user, and render JSON with the serializer’s serialize_new_post() method.

In PostSerializer, I pull together details about the post, including the URL that will redirect our end user to the image hosted by Cloudinary. If it seems odd to explicitly pass the instance variable @post to the instance method serialize_post, ignore it — it’s a requirement of other PostSerializer functions that aren’t relevant to this post. If you’re curious, the full source code is here. Similarly, the contents of the serialize_user_details method are unimportant.

But how, exactly, does post.get_image_url() work, and where does it come from?

This is a method I defined on the Post model itself, the image URL being a pseudo-attribute of the post. It made sense to me that the post should know about its image URL.

To access the URL that ActiveStorage creates for each image, we use Rails’ url_for() method. But there’s a snag: Models don’t normally have access to Rails’ url_helpers. It’s necessary to include Rails.application.routes.url_helpers at the top of the class before you can use it.

If you try to hit your endpoint from the front end at this stage, you’ll likely see this error on the back end:

ArgumentError (Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true)

To resolve it, navigate to config/environments/development.rb and add Rails.application.routes.default_url_options = { host: "http://localhost:3000" } (or your preferred development port, if not 3000). In ./production.rb, do the same, using the web root of your production server as the host value.

If it’s all working correctly, your endpoint will now return beautifully formatted JSON that includes an image link. When clicked or loaded, it will redirect to your Cloudinary-hosted image.

Commit your work, push it to Github, and breathe a sigh of relief.

Better Programming

Advice for programmers.

Angus Morrison

Written by

Software Engineer @ Bamboo. Recovering product manager and UX Consultant.

Better Programming

Advice for programmers.

More From Medium

More from Better Programming

More from Better Programming

More from Better Programming

More from Better Programming

Why Do Incompetent Managers Get Promoted?

546

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade