Building an Instagram clone with Ruby on Rails: Part 1

In this multi-part tutorial, we will be building a Ruby on Rails app that implements the look and the features of popular social media giant Instagram. We’ll use Rails 6, Webpacker 4, Turbolinks, Stimulus Js and Tailwindcss.

DISCLAIMER: While the purpose of this tutorial is to share knowledge and show how to implement certain features for learning, due to its length and text format, it’s not aimed at the novice Rails developer and it’s not an introduction to Rails. If you’re still unfamiliar with the structure of a Rails application, I recommend starting with some basic Rails tutorials first.

I’ve set up a Rails 6 app (rc1 version) with Webpacker 4, Turbolinks and the upcoming release 1.0 of Tailwindcss. If you want to follow along you can clone the app from the following branch:

git clone -b tutorial-start

To run the boilerplate, you’ll need to have ruby 2.5.3 installed in your machine and then run from the terminal inside the cloned project folder:

bundle install
yarn install
rails db:create db:migrate db:seed
rails s

The goal is to focus more on the Rails way of implementing features (backend and frontend) and less on the HTML and CSS parts, so we won't be diving too deep into those topics. As such, the above branch already has some styling applied with Tailwindcss and Fontawesome recreating the Instagram look.

Before we start though, a brief introduction to Tailwindcss:

Unlike Bootstrap, Foundation, Bulma, Tailwindcss is not a UI kit (there is no theme or there are no built-in UI components). According to the documentation:

Tailwind provides highly composable, low-level utility classes that make it easy to build complex user interfaces without encouraging any two sites to look the same.

It is responsive and provides tools for extracting component classes from repeated utility patterns, making it easy to build our own custom UI components:

// Using Tailwind's utility classes to 
// create variations of buttons
.btn-blue {
@apply bg-blue-500 text-white;
.btn-white {
@apply text-blue-500
.btn-white:focus {
@apply text-blue-400

Tailwind is written in PostCSS and configured in JavaScript. I have therefore placed the components in app/javascript/stylesheets to be compiled by Webpack. You can find the main Tailwind config file in app/javascript/stylesheets/config/tailwind.config.js

The User Model

We’re using Devise to sign up and log in in users. Instagram requires users to sign up with their full name and username in addition to the usual email and password which Devise already includes by default.

Let’s generate a migration in the terminal:

rails g migration AddAttributesToUsers full_name username:string:uniq about:text

This generates the following migration:

Instagram uses the username to generate the URLs of user profiles. As such, usernames must be unique, so we’re adding that constraint to the database in the migration. It also adds an about column which we will implement later.

Don’t forget to rails db:migrate to apply the migration.

Let’s also add a validation to the User model that reflects the uniqueness requirement. We also want to ensure that usernames are only made up of alphanumerical characters and are not exclusively comprised of numbers. We’ll use a regular expression for that. The validation should also be case insensitive.

validates :username,presence: true,
format: { with: /\A(?=.*[a-z])[a-z\d]+\Z/i },
uniqueness: { case_sensitive: false }

Instagram doesn’t require users to confirm the password and allows them to sign in with either their email or username. Go to the devise generated views and remove the password confirmation field from registrations#new and sessions#new.

We need to allow users to provide additional parameters on the sign-up form. Add this to the ApplicationController.

This allows the additional attributes to be added to Devise’s strong params.

We’ll need to modify our navbar to display the user avatar and a dropdown so that logged in users can sign out. You can see the updated code here:

Note: If you’re following along the tutorial, you’ll also need to add the following to the tailwind config file:

// app/javascript/stylesheets/config/tailwind.config.jsvariants: {
borderColors: ['responsive', 'hover', 'focus', 'group-hover'],
visibility: ['responsive', 'group-hover'],

And create a dropdown CSS component:

We’ll also use a view helper to determine the correct user avatar URL. Until we implement image uploads, there are only two options: we check if the user email is associated with a Gravatar account. If not, we display a default user avatar image:

Allowing Users to Sign In With the Username

By default, Devise allows users to sign in and to reset their password by providing their email address. Instagram allows users to supply either the username or the email address. We’ll need to override Devise defaults for this.

Add login as an attribute accessor to the User model: attr_accessor :login

Modify config/initializers/devise.rb to have:

config.authentication_keys = [:login]

And override Devise’s find_for_database_authentication method in the User model:

You can follow this wiki to do the same for the password recovery:

At this point, you should test your application to see if it works before moving on.

Refactoring the User model with a model Concern:

By now, your User model is starting to look quite fat, with lots of devise methods, and we’ve only just begun. Let’s extract all of this code into a model concern. Create a file in app/models/concerns/authenticable.rb and move all that code from the model into this file:

Then all you have to do is add include Authenticable to the User model. Make sure to test it again.

User Profile

Instagram uses the usernames at the root of their URL to access user profiles. To do that, we need the following route:

resources :users, path: '/', param: :username, only: %i[show]

This creates a show route at the root of our application that is capturing the username in the params and sends it to the userscontroller#show. Then we need to tell our User model how to generate the URL parameters for itself:

# User 
def to_param

Here I’ve added a few more classes to style the Front End and give it the Instagram feel. Remember, we’re using Tailwindcss, so all our custom components are being created in app/javascript/stylesheets/components. You can check out the full code in the Github repository shared at the end of this tutorial.

Following Users

Finally, we’re going to add the ability to follow other users. This can be achieved with a join table that connects two users: a follower, referenced by a column follower_id, and a followee or the user the follower is following, referenced by a column following_id. Let’s generate the model:

rails g model Follow

Amend the migration accordingly:

And extract the code into a relevant concern:

Update the routes:

We’ll namespace the FollowsController under users and use a create action (named follow_path) that calls the follow method we implemented above in the concern and a destroy action (named unfollow_path) that calls the unfollow action implemented in the same concern. Both actions live under the same endpoint /:username/follow and we use the HTTP verbs POST and DELETE to differentiate between the two. (you can read a more in-depth article on the possibilities of Rails routing here).

First, we make sure that the follow/unfollow button in the view works correctly via a regular HTML request. Then, we ajaxify it by adding the option remote: true to those links. This sends an AJAX request to the backend instead of reloading the whole page and then we update the button and the corresponding link with javascript. I’ve extracted the button code into a view partial for this purpose: app/views/users/_follow_btn.html.erb

Counter Caching

We want to display in the profile page the number of followers a user has and the number of users a user is following as Instagram does. We could use ActiveRecord to query the database and count the relevant Follow records, but we’ll make use of a Rails feature called counter caching.

First, we need to add two columns to our User model that will keep track of those two counters with the following migration:

Then we add the counter_cache option to the Follow model. This instructs Rails to update the User columns with the correct count of follower and following each time a Follow record is created or destroyed.

Then update our view accordingly with these two new columns and also edit create.js.erb to ensure the view gets updated during AJAX requests:

Since the @user instance is loaded before the counter_caches get triggered and before the counter columns get updated every time a user follows or unfollows another user, we need to reload the instance in order to display the correct number of followers and following. We do this by adding @user.reload inside the block in the create and destroy actions of the FollowsController.

Finally, we’ll add Stimulus to take care of the pluralization of the followers count. We could use Rails pluralize method, but Stimulus will come in handy later on as well.

Install stimulus in the terminal:

bundle exec rails webpacker:install:stimulus

Stimulus works by sprinkling HTML elements in the view with data attributes that reference stimulus controllers and targets and event actions. In our users#show view, we add the controller to the header element: <headerclass="flex mb-16" data-controller="follow-button">.

We then need to specify the target element where we will read the actual number of followers, and the target where we will inject the word Follower in it's singular or plural form. This is done by adding a data-target attribute to the relevant elements:

Then we set up our follow/unfollow button to listen to the ajax:success event with a data-action attribute and update the follower message/word with stimulus:

And the stimulus controller looks like this:

That’s it for Part 1 of this tutorial. We’ll continue our Instagram clone in Part 2. You can check the full code for this part here 👉

NOTE: Many thanks to Juliette Chevalier for improvement suggestions to this article and for stopping typos and errors.



Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Rui Freitas

Rui Freitas

Lead Teacher @ Le Wagon | Web Developer @ Light the Fuse and Run: | Photographer @ Rod Loboz: