Simple Instagram app with Rails 6 API and React. Part 1
Building a simple Instagram clone with Rails 6 API and React.
The Rails Part.
find the react part here
In this first version of our API we will build without users and authentication. We also won’t have comments in this version. This version is intended to get you up and running using rails and react quickly.
We would write tests and use git for version control locally.
Code is available here
- generate an API
$ rails new instabox-api — api -T
b.
git init
git checkout -b master add README.md
git commit 'first commit'
git checkout -b development add .
git commit -m "initial commit"
git checkout -b setup-models
2.Install gems for tests
gem ‘rspec-rails’, ‘~> 3.5’group :test do
gem ‘factory_bot_rails’, ‘~> 4.0’
gem ‘shoulda-matchers’, ‘~> 3.1’
gem ‘faker’
gem ‘database_cleaner’
end
3.
bundle install
4. We’re TDD so we’ll be writing tests.
rails g rspec:install to setup rspec
5. copy and paste the following into your spec/rails_helper.
# This file is copied to spec/ when you run 'rails generate rspec:install'
ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../../config/environment', __FILE__)abort("The Rails environment is running in production mode!") if Rails.env.production?
require 'spec_helper'
require 'rspec/rails'require 'database_cleaner'Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }ActiveRecord::Migration.maintain_test_schema!Shoulda::Matchers.configure do |config|
config.integrate do |with|
with.test_framework :rspec
with.library :rails
end
endRSpec.configure do |config|config.fixture_path = "#{::Rails.root}/spec/fixtures"config.use_transactional_fixtures = trueconfig.include FactoryBot::Syntax::Methodsconfig.before(:suite) do
DatabaseCleaner.clean_with(:truncation)
DatabaseCleaner.strategy = :transaction
endconfig.around(:each) do |example|
DatabaseCleaner.cleaning do
example.run
end
end
config.infer_spec_type_from_file_location!config.include RequestSpecHelperconfig.filter_rails_from_backtrace!end
6.generate our models. In rails, models are always written in singular forms. i.e without an ‘s’.
rails g model Picture img_link:string likes:integer liked:boolean created_by:string
7.
rails db:migrate # this command creates the db schema.
Navigate to spec > models > picture_spec.rb. Add the following code to test our models.
spec > models > picture_spec.rbrequire ‘rails_helper’RSpec.describe Picture, type: :model do
# checks that the img link isn't empty before adding to db
it { should validate_presence_of(:img_link) }
it { should validate_presence_of(:created_by) }
endrspec
both tests should fail as we have not written our models logic yet.
Navigate to models and add the following code to always check that each picture has an image link and created_by column that is not empty.
app > models > picture.rbclass Picture < ApplicationRecord
validates_presence_of :img_link, :created_by
end
12. run
rspec
this time your tests should be green.
13. make a commit
git add .
git commit -m "models done with tests"
14. Go back to your development branch(working code is saved here). Merge your models into your development branch. Checkout to a new branch called controllers.
git checkout development
git merge modelsgit checkout -b controllers
15. We create our controllers and controller actions. Controllers check all the actions a user might want to perform on data in a database and helps us perform those actions. For our app all we want to do is display all the pictures in one list(index action), post a picture(create), show details about a picture(show), delete a picture(destroy)
rails g controller Pictures index create show destroy
16. When we build APIs we write request tests instead of conventional controller tests.
17.mkdir spec/requests && touch spec/requests/pictures_spec.rb
18. create a pictures.rb to generate the factory data inside the factories folder.
touch spec/factories/pictures.rb
19. We use the Factory gem to create dummy data for our tests.
spec > factories > pictures.rbFactoryBot.define do
factory :picture do
img_link 'imglink.io'
likes '10'
liked false
caption 'caption'
created_by 'creator'
end
end
20. Here are the request specs. Thanks to Austin Kabiru for his well written article on rails APIs and testing. The tests in this guide are based on his documentation.
require 'rails_helper'RSpec.describe 'Pictures API', type: :request do
# initialize test data
let!(:pictures) { create_list(:picture, 10) }
let(:picture_id) { pictures.first.id }# Test suite for GET /pictures
describe 'GET /pictures' do
# make HTTP get request before each example
before { get '/pictures' }it 'returns pictures' do
# Note `json` is a custom helper to parse JSON responses
expect(json).not_to be_empty
expect(json.size).to eq(10)
endit 'returns status code 200' do
expect(response).to have_http_status(200)
end
end# Test suite for GET /pictures/:id
describe 'GET /pictures/:id' do
before { get "/pictures/#{picture_id}" }context 'when the record exists' do
it 'returns the picture' do
expect(json).not_to be_empty
expect(json['id']).to eq(picture_id)
endit 'returns status code 200' do
expect(response).to have_http_status(200)
end
endcontext 'when the record does not exist' do
let(:picture_id) { 100 }it 'returns status code 404' do
expect(response).to have_http_status(404)
endit 'returns a not found message' do
expect(response.body).to match(/Couldn't find picture/)
end
end
end# Test suite for POST /pictures
describe 'POST /pictures' do
# valid payload
let(:valid_attributes) { { title: 'Learn Elm', created_by: '1' } }context 'when the request is valid' do
before { post '/pictures', params: valid_attributes }it 'creates a picture' do
expect(json['title']).to eq('Learn Elm')
endit 'returns status code 201' do
expect(response).to have_http_status(201)
end
endcontext 'when the request is invalid' do
before { post '/pictures', params: { title: 'Foobar' } }it 'returns status code 422' do
expect(response).to have_http_status(422)
endit 'returns a validation failure message' do
expect(response.body)
.to match(/Validation failed: Created by can't be blank/)
end
end
end# Test suite for DELETE /pictures/:id
describe 'DELETE /pictures/:id' do
before { delete "/pictures/#{picture_id}" }it 'returns status code 204' do
expect(response).to have_http_status(204)
end
end
end
We start by populating the database with a list of 10 picture records (thanks to factory bot). We also have a custom helper method json
which parses the JSON response to a Ruby Hash which is easier to work with in our tests. Let's define it in spec/support/request_spec_helper
.
Add the directory and file:
mkdir spec/support && touch spec/support/request_spec_helper.rb
run tests
rspec
Some tests would fail and some would pass. The passing tests are because we ran the rails g controller index create show delete command. Now we would use resources instead of the get routes we currently see in the config.rb file. Using resources: offers us all the HTTP methods without us having to define them manually.
config > routes.rbRails.application.routes.draw do
resources :pictures, only: [:show, :destroy, :index, :create, :update]
end
We only need HTTP methods for the 5 actions show, destroy, index , update and create. Now when you run rails routes, you can see all the routes we have available to us.
Now, let’s head to our controllers to define our actions.
app > controllers > pictures_controller.rbclass PicturesController < ApplicationController
# see line 37-38
before_action :set_picture, only: [:show, :destroy, :update]def index
# this orders all pictures starting with the last created
@pictures = Picture.all.order(created_at: :desc)
render json: @pictures, status: :ok
end# creates an instance of a picture and returns the status
# "created" if the action is successful
def create
@picture = Picture.create!(picture_params)
render json: @picture, status: :created
end# allows us add or reduce likes from the front end
def update
@pictures # we need to send the entire set of pictures back.
@picture.update(picture_params)# update a specific pic
end# deletes a picture
def destroy
@picture.destroy
head :no_content
end# displays more info about a picture
def show
render json: @picture, status: :ok
endprivate
# In rails we need this method to allow us collect/whitelist
# information from the user to store in our DB
def picture_params
params.permit(:img_link, :caption, :created_by, :likes, :liked)
end# we need to grab the id of the intended picture
# before we can run show more info or delete it.
def set_picture
@picture = Picture.find(params[:id])
end
end
We also need exception handlers for when thing go wrong in our controller. Our friend Kabiru has also written a nice module for that.
app > controllers > concerns > exception_handler.rbmodule ExceptionHandler
extend ActiveSupport::Concern
included do
rescue_from ActiveRecord::RecordNotFound do |e|
json_response({ message: e.message }, :not_found)
end
rescue_from ActiveRecord::RecordInvalid do |e|
json_response({ message: e.message }, :unprocessable_entity)
end
end
end
Create another module called json_response.rb inside the concerns folder.
app > controllers > concerns > json_response.rb
module Response
def json_response(object, status = :ok)
render json: object, status: status
end
end
One last thing, our Controllers do not know we have this exception handlers. Head over the your application controller to add this line.
class ApplicationController < ActionController::API
include Response
include ExceptionHandler
end
Time to test if all the things we have written actually work.
I use Postman for my manual tests.
First start your rails server.
rails s
Open up postman.
- Select the POST method (create action in our controllers).
- Head over to the body tab and select x-www-form-urlencoded. You need to have this checked to make POST requests.
- type out the following data(or yours) into the “Key” and “Value” columns.
- Hit send.
- You should get data back with status:201 Created, just as we defined it in our controllers!
Let’s make a GET request. (index action)
Let’s test out our show action. If you don’t know what routes to use(link) type rails routes in your console/terminal.
Let’s get the data from Picture with ID:2. We’re making a request to our Show action.
Now Let’s DELETE the first picture record I created initially.
As you can see we get a status of 204 — No content. Just as we defined it in our controller.
You can test the update route using the same logic from above. Remember, you’d be making a PUT request.
If you check the console where you have your rails server running, at the end of each action, you can observe what your console outputs.
Now for automated testing using RSpec. simply run rspec and our tests should come back green.
We need to add one more gem to allow our backend server communicate with our frontend app.
Add this to your gemfile. Or if it’s already in your gemfile, uncomment it.
gem ‘rack-cors’, require: ‘rack/cors’
Then run
bundle install
Head over to config > initializes > cors.rb and uncomment the lines starting from Rails.application.It should look like below.
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins 'http://localhost:3000' # the URL for our frontend app
resource '*',
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options, :head],
credentials:true # adding this for when we decide to build authend
end
Now that everything seems to be working. Let’s make a commit.
check that you’re on the controllers branch with
git branch
then run
git add .
git commit -m "controller actions complete"
git checkout development
git merge controllers
Let’s initialize our database with some data.
Head to db > seeds.rb and add the following data
Picture.create!(
img_link: 'https://images.unsplash.com/photo-1517533564579-4e4cef1505c4?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=500&q=60',
likes: 2,
created_by: 'Osas',
liked: 'true',
caption: 'Picture by Sharon Garcia from Unsplash')Picture.create!(
img_link: 'https://images.unsplash.com/photo-1555029510-40401d84c73c?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=634&q=80',
likes: 0,
created_by: 'Chisom',
liked: 'false',
caption: 'Picture by Olu Famule from Unsplash')
This will create 2 pictures in our DB. You can confirm this by checking the rails console.
> rails db:seed # this command populates our DB
> rails c # opens up the rails console
> Picture.connection # creates a connection to our Picture table
> Picture.all
You should see our freshly uploaded data here. Type exit and hit enter to exit the console.
Everything looks good!
Make a commit. Merge to development and let’s deploy to heroku.
Heroku deployment
- Head to your heroku dashboard. Create an account if you don’t have one.
- Click ‘New’ > ‘Create New App’
- Give your app a name. Mine is ‘instabox-api’. Click create app.
- Follow the instructions to install the heroku cli and login.
- We already have git initialized locally. All we need to do now is reference our heroku repo. heroku git:remote -a instabox-api. Add this in your terminal the root directory of our project.
- Then
git push heroku master
If everything goes well, you should see a notification that your app was successfully deployed.
Turns out we forgot to swap our sqlite3 db(comes default with rails) for postgresql(heroku’s db). Let’s make that change now.
#drop the db
rails db:drop# add pg gem
gem ‘pg’, ‘>= 0.18’, ‘< 2.0’comment out sqlite3bundle install
copy and paste the following code
default: &default
adapter: postgresql
encoding: unicode
# For details on connection pooling, see Rails configuration guide
# https://guides.rubyonrails.org/configuring.html#database-pooling
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>development:
<<: *default
database: instabox_apitest:
<<: *default
database: instabox_api_testproduction:
<<: *default
database: instabox_api_production
username: instabox_api
password: <%= ENV['INSTABOX_API_DATABASE_PASSWORD'] %>
Recreate the db, make a migration and then seed our data.
rails db:create db:migrate db:seed
Then make another commit before pushing to heroku
> while on the master branchgit add .
git commit -m “update db to pgsq”
git push heroku master
This time around, our deployment should be successful. One last step. I missed this the last time and I spent hours trying to figure out why I can’t see my data.
heroku run rake db:migrate
heroku run rake db:seed
If you miss the two steps above, your db won’t have any data.
If you head to https://instabox-api.herokuapp.com/pictures. You should see our data in JSON format. Sometimes it takes a while to complete the build and display the data.
find the react part here