03/01/2020

Let's Build a CRUD App with Ruby on Rails and React

A step-by-step walk through to building a CRUD app with react and ruby on rails. In this article we will be building a flight reviews app from scratch with basic CRUD functionality.

In 2018, I made a short video demonstrating how to build a react app with ruby on rails using webpacker. Since then, a few people have asked me to do a follow up to this that focuses specifically on building a CRUD app with this stack.

build-a-crud-app-comment-1

build-a-crud-app-comment-2

So in this post, we are going to build a CRUD app from start to finish using react and ruby on rails with webpacker. The app we will be building is an airline review app. This is what the final version of the app will look like:

Open Flights Index

Open Flights Show

To paraphrase this guy a little, I'm mainly writing this article for 3 reasons:

  • 1. To share it with you.
  • 2. To document my current process for future reference/implementation.
  • 3. To learn from your feedback so I can improve this.

Prerequisites and Notes

This post will assume you already have at least some beginner level knowledge of both ruby on rails and react. I'll be using postgresql for my database in this article, but if you are using sqlite or mysql everything should work just the same.

In various code block examples, I'll use ... to indicate that there is additional code above or below the example. In many of the code snippets in this post, I'll use yellow dashed underlines to try to highlight the specific piece of code we are adding, although I may not use it in every example.

Table of Contents

Getting Started: Creating a New Rails App With React & Webpacker

First things first, let's create a brand new rails app. We can do this from the command line by doing rails new app-name where app-name is the name of our app, however we are going to add a few additional things. We need to add --webpack=react to configure our new app with webpacker to use react, and additionally I'm going to add --database=postgresql to configure my app to use postgres as the default database. so the final output to create our new app will look like this:

rails new open-flights --webpack=react --database=postgresql

Sidenote: if you are reading this and you instead want to add react to an existing rails app, you can do so by adding webpacker to your Gemfile, running bundle install, and then running bundle exec rails webpacker:install:react from your CLI

Once this finishes running, make sure to cd into the directory of your new rails app (cd open-flights), then we can go ahead and create the database for our app by entering the following into our command line:

rails db:create

Models

Our data model for this app will be pretty simple. Our app will have airlines, and each airline in our app will have many reviews.

For our airlines, we want to have a name for each airline, a unique url-safe slug, and an image_url for airline logos (Note: I'm not going to handle file uploading in this post, instead we will just link to an image hosted on s3).

For our reviews, we want to have a title, description, score, and the airline_id for the airline the review will belong to. The scoring system I'm going to use for our reviews will be a star rating system that ranges from 1 to 5 stars; 1 being the worst score and 5 being the best score.

So from our command line we can enter the following generators to create our airline and review models in our app:

rails g model Airline name slug image_url
rails g model Review title description score:integer airline:belongs_to

Note: using airline:belongs_to with our generator is an easy way we can establish the belongs_to relationship between airlines and reviews in our app. This will also handle setting the foreign key on the table and even create an index for us!

Note: Rails uses ActiveRecord, which tightly couples our models with the structure of our database. When we use a generator to create new models with specific attributes, we are additionally creating migrations to add a new table for each one in our database. These tables will have fields that correspond to our attributes, although we can modify/add to this before we run our migrations to create the tables.

This will create two new files in our db/migrations folder; one for airlines:


class CreateAirlines < ActiveRecord::Migration[5.2]
  def change
    create_table :airlines do |t|
      t.string :name
      t.string :slug
      t.string :image_url

      t.timestamps
    end
  end
end

and one for reviews:


class CreateReviews < ActiveRecord::Migration[5.2]
  def change
    create_table :reviews do |t|
      t.string :title
      t.string :description
      t.integer :score
      t.belongs_to :airline, foreign_key: true

      t.timestamps
    end
  end
end

Additionally, we should now have airline and review model files created for us inside of our app/models directory. Because we used airline:belongs_to when we generated our review model, this model should already have the belongs_to relationship established, so our review model so far should look like this:

filepath: app/models/review.rb

class Review < ApplicationRecord
  belongs_to :airline
end

We need to additionally add has_many :reviews to our airline model. Once we do, our airline model should look like this:

filepath: app/models/airline.rb

class Airline < ApplicationRecord
  has_many :reviews
end

At this point, let's go ahead and migrate our database:

rails db:migrate

Once you run that, you should see a new schema.rb file created within the db folder in our app. Your schema file should now look something like this:

filepath: db/schema.rb

ActiveRecord::Schema.define(version: 2019_12_26_200455) do
  enable_extension "plpgsql"

  create_table "airlines", force: :cascade do |t|
    t.string "name"
    t.string "slug"
    t.string "image_url"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  create_table "reviews", force: :cascade do |t|
    t.string "title"
    t.string "description"
    t.integer "score"
    t.bigint "airline_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["airline_id"], name: "index_reviews_on_airline_id"
  end

  add_foreign_key "reviews", "airlines"
end

      

So now for our airline model, we need to do a couple things. First off, I want to add a before_create callback method that creates a unique slug based off of the airline's name when we create a new airline. To do this, we can add a new slugify method with a before create callback to our airline model like this:

filepath: app/models/airline.rb

class Airline < ApplicationRecord
  has_many :reviews

  before_create :slugify

  def slugify
    self.slug = name.downcase.gsub(' ', '-')
  end
end
    

This slugify method will take the name of an airline, convert any uppercase characters to lowercase, replace any spaces with hyphens, and set this value as our slug before saving the record.

Actually, I think we can simplify this method further by just calling parameterize on our name attribute instead of using downcase and gsub:

filepath: app/models/airline.rb

class Airline < ApplicationRecord
  has_many :reviews

  before_create :slugify

  def slugify
    self.slug = name.parameterize
  end
end
    

This parameterize method should handle both downcasing characters and replacing spaces with hyphens for us. Of course, we can quickly test this out from our rails console to confirm:


'Fake AIRline Name     1'.parameterize
=> "fake-airline-name-1"

So now if/when we create a new airline, for example United Airlines, this will convert the name to united-airlines and set it as the slug for that airline.

Additionally, we need to create a method that will take all of the reviews that belong to an airline and get the average overall rating. We can add an avg_score method to our model like this:

filepath: app/models/airline.rb

class Airline < ApplicationRecord

  ...

  def avg_score
    return 0 unless reviews.size.positive?

    reviews.average(:score).to_f.round(2)
  end
end

This method will return 0 if an airline has no reviews yet. Otherwise it will get the average of all the review scores for an airline.

Note: You may notice when we create our serializers momentarily that managing the average score in this way will lead to n+1 queries in our code. We will fix this at a later point.

Thanks Jonny Marshall for pointing this out!

So our full Airline model with our slugify method and avg_score method should now look like this:

filepath: app/models/airline.rb

class Airline < ApplicationRecord
  has_many :reviews

  before_create :slugify

  def slugify
    self.slug = name.parameterize
  end

  def avg_score
    return 0 unless reviews.size.positive?

    reviews.average(:score).to_f.round(2)
  end
end

Seeding Our Database

Now that we've got our models created, let's go ahead and seed our database with some data! We can add this to the seeds.rb file located inside of our db folder:

filepath: db/seeds.rb

Airline.create([
  { 
    name: "United Airlines",
    image_url: "https://open-flights.s3.amazonaws.com/United-Airlines.png"
  }, 
  { 
    name: "Southwest",
    image_url: "https://open-flights.s3.amazonaws.com/Southwest-Airlines.png"
  },
  { 
    name: "Delta",
    image_url: "https://open-flights.s3.amazonaws.com/Delta.png" 
  }, 
  { 
    name: "Alaska Airlines",
    image_url: "https://open-flights.s3.amazonaws.com/Alaska-Airlines.png" 
  }, 
  { 
    name: "JetBlue",
    image_url: "https://open-flights.s3.amazonaws.com/JetBlue.png" 
  }, 
  { 
    name: "American Airlines",
    image_url: "https://open-flights.s3.amazonaws.com/American-Airlines.png" 
  }
])
      

And then we can seed our database by running the following command in our terminal:

rails db:seed

Now if we jump into our rails console with rails c we should be able to see our new data in the database:


irb(main):001:0> Airline.first
Airline Load (0.3ms)  SELECT  "airlines".* FROM "airlines" ORDER BY "airlines"."id" ASC LIMIT $1  [["LIMIT", 1]]
=> #<Airline id: 1, name: "United Airlines", slug: "united-airlines", image_url: "https://open-flights.s3.amazonaws.com/United-Airlines.png", created_at: "2019-12-26 23:02:58", updated_at: "2019-12-26 23:02:58">
      

Notice that even though we only included the name and image_url in our seed data, we additionally have a slug value (in this case "united-airlines") because we added that slugify method to our airline model. We will use this slug shortly as the paramater to find records by in our controllers, instead of using the id param.

Serializers: Building Our JSON API

For our app we are going to use fast_jsonapi, a gem created by the Netflix engineering team. If you have ever used Active Model Serializer (AMS), you will likely notice some similarities.

with fast_jsonapi, we can create the exact structure for the data we want to expose in our api, and then use that when we render json from within our controllers.

Let's install the fast_jsonapi gem, by adding it to our Gemfile:

filepath: Gemfile
gem 'fast_jsonapi'

Then we can install it with bundle install from our terminal:

bundle install

Now we can use a generator to create a new airline serializer and review serializer, passing along the specific attributes we want to expose in our api:

rails g serializer Airline name slug image_url

rails g serializer Review title description score airline_id

This will create a new serializer folder in our app and create a new airline serializer that should so far look like this:

filepath: app/serializers/airline_serializer.rb

class AirlineSerializer
  include FastJsonapi::ObjectSerializer
  attributes :name, :slug, :image_url
end
      

And a reviews serializer that should look like this:

filepath: app/serializers/review_serializer.rb

class ReviewSerializer
  include FastJsonapi::ObjectSerializer
  attributes :title, :description, :score, :airline_id
end        
      

For our airlines serializer, we want to include the relationship with reviews in our serialized json. We can add this simply by adding has_many :reviews into our serializer. So then our serializer should look like this:

filepath: app/serializers/airline_serializer.rb

class AirlineSerializer
  include FastJsonapi::ObjectSerializer
  attributes :name, :slug, :image_url
  has_many :reviews
end
      

Let's take a quick look at how we can use our serializers now to structure our api. If we jump into a rails console (rails c) in our terminal, let's get the first airline from our database. Then we can initialize a new instance of our airline serializer with that record and return the result as serialized json:


# Get the first airline record from our database
airline = Airline.first
=> #<Airline id: 1, name: "United Airlines", slug: "united-airlines", image_url: "https://open-flights.s3.amazonaws.com/United-Airlines.png", created_at: "2019-12-26 23:02:58", updated_at: "2019-12-26 23:02:58">

# Serialized JSON
AirlineSerializer.new(airline).serialized_json
=> "{\"data\":{\"id\":\"1\",\"type\":\"airline\",\"attributes\":{\"name\":\"United Airlines\",\"slug\":\"united-airlines\",\"image_url\":\"https://open-flights.s3.amazonaws.com/United-Airlines.png\"},\"relationships\":{\"reviews\":{\"data\":[]}}}}"

# Formatted JSON
AirlineSerializer.new(airline).as_json
=> {
  "data" => {
    "id" => "1", 
    "type" => "airline", 
    "attributes" =>  {
      "name" => "United Airlines", 
      "slug" => "united-airlines", 
      "image_url" => "https://open-flights.s3.amazonaws.com/United-Airlines.png"
    }, 
    "relationships" => {
      "reviews" => {
        "data" => []
      }
    }
  }
}
      

In the above examples, you can see that the only attributes shared within the attributes section are those that we have explicitly declared in our airline seriaizer.

Controllers

Our app is going to have three controllers: an airlines controller, a reviews controller and a pages controller. Our pages controller will have a single index action that I'm going to use as the root path of our app. I'm also going to use Pages#index as a sort of catch-all for any requests outside of our api. This will come in handy once we start using react-router in a little, as we will need to be able to match routes to different components.

For our airlines and reviews controllers, we are going to namespace everything under api/v1. Again, this will give us an easy way to manage routing from both the react side of our app and the rails side once we additionally start using react-router in a moment.

For example, if a user navigates to /airlines in our app, on the react side we can load the necessary components to show a list of all airlines, and on the back end we can make the request to our Airline#index action in our controller as /api/v1/airlines to get a list of all of the airlines from our api.

Routes

Let's actually go ahead and set up our routes, adding our root path and our namespaced api resources:

filepath: config/routes.rb

Rails.application.routes.draw do

  root 'pages#index'

  namespace :api do
    namespace :v1 do
      resources :airlines, param: :slug
      resources :reviews, only: [:create, :destroy]
    end
  end

  get '*path', to: 'pages#index', via: :all
end
      

In my routes I have added get '*path', to: 'pages#index', via: :all. This will route any requests that aren't for existing paths under api/v1 back to our index path. The reason I'm doing this is that once we start using react-router, this will let us handle routing to react components while also not interfering with our defined routes for our rails api. It's important that this line goes at the end of your routes.rb file and not before the api routes being defined.

Notice that I added param: :slug to our airlines resources so that we can use our slugs as the primary param for airlines instead of using id.

Pages Controller

All that we need to do for this for now is set up a new pages controller in our controllers folder and create a corresponding index action method. We can do this with a generator by simply entering the following into our terminal:

rails g controller pages index

Airlines Controller

Inside of app/controllers, let's create a new api folder, and inside of that, a new v1 folder, and then inside of that let's create a new airlines controller, namespaced under Api::V1:

filepath: app/controllers/api/v1/airlines_controller.rb

module Api
  module V1
    class AirlinesController < ApplicationController
    end
  end
end

Airlines#index

Now let's add an index method to our new controller. All we need to do for this method is get all of the airlines from our database, then render the data as JSON using our AirlineSerializer.

To get all of our airlines, we can simply call all on our Airline model like so:

airlines = Airline.all

Then we can pass our airlines variable as an argument into a new instance of our AirlineSerializer and return our data as serialized JSON like so:

AirlineSerializer.new(airlines).serialized_json

So putting these two steps together, and then rendering the result as JSON from our controller, our index method should look like this:

filepath: app/controllers/api/v1/airlines_controller.rb

module Api
  module V1
    class AirlinesController < ApplicationController
      def index
        airlines = Airline.all

        render json: AirlineSerializer.new(airlines).serialized_json
      end
    end
  end
end

Airlines#show

Our show method will also be pretty simple. For this we just need to find a specific airline, not by its id, but using it's slug as the param. We can do this by calling find_by on our Airline model and searching for a record that has a matching slug, like so:

airline = Airline.find_by(slug: params[:slug])

Then, we will again render the resulting JSON using our AirlineSerializer. So our show method should look like this:

filepath: app/controllers/api/v1/airlines_controller.rb

module Api
  module V1
    class AirlinesController < ApplicationController
      ...
      
      def show
        airline = Airline.find_by(slug: params[:slug])

        render json: AirlineSerializer.new(airlines).serialized_json
      end
    end
  end
end

Airlines#create

Before we add our create method, let's use strong paramaters to create a whitelist of allowed parameters when creating a new airline in our app. For now we will allow only name and image_url:

filepath: app/controllers/api/v1/airlines_controller.rb

module Api
  module V1
    class AirlinesController < ApplicationController
      
      ... 

      private

      def airline_params
        params.require(:airline).permit(:name, :image_url)
      end
    end
  end
end

Then we can go ahead and add our create method. For this, we will simply initialize a new instance of Airline, passing in our airline_params. If everything is valid and saves, we will render data for our new airline again using our airline serializer, otherwise we will return an error:

filepath: app/controllers/api/v1/airlines_controller.rb

module Api
  module V1
    class AirlinesController < ApplicationController

      ...

      def create
        airline = Airline.new(airline_params)

        if airline.save
          render json: AirlineSerializer.new(airline).serialized_json
        else
          render json: { error: airline.errors.messages }, status: 422
        end
      end

      private

      def airline_params
        params.require(:airline).permit(:name, :image_url)
      end
    end
  end
end

Airlines#update

For our update method we can take a more or less similar approach to what we did in our create method. However, because we are modifying an existing record, we need to first find that record by it's slug like we did in our show method. Then, instead of calling save, we want to update the record, again passing in our airline_params. So here is what our update method will look like then:

filepath: app/controllers/api/v1/airlines_controller.rb

module Api
  module V1
    class AirlinesController < ApplicationController

      ...

      def update
        airline = Airline.find_by(slug: params[:slug])

        if airline.update(airline_params)
          render json: AirlineSerializer.new(airline).serialized_json
        else
          render json: { error: airline.errors.messages }, status: 422
        end
      end

      ...

    end
  end
end

Airlines#destroy

For our destroy method, we will again find the record to destroy by its slug. Then we can call destroy on it. If everything works, we will render nothing with a success response code indicating that the deletion worked. Otherwise, we will return an error.

filepath: app/controllers/api/v1/airlines_controller.rb

module Api
  module V1
    class AirlinesController < ApplicationController

      ...

      def destroy
        airline = Airline.find_by(slug: params[:slug])

        if airline.destroy
          head :no_content
        else
          render json: { errors: airline.errors.messages }, status: 422
        end
      end

      ...

    end
  end
end

So now, our full airlines controller should look like this:

filepath: app/controllers/api/v1/airlines_controller.rb

module Api
  module V1
    class AirlinesController < ApplicationController
      def index
        airlines = Airline.all

        render json: AirlineSerializer.new(airlines).serialized_json
      end

      def show
        airline = Airline.find_by(slug: params[:slug])

        render json: AirlineSerializer.new(airline).serialized_json
      end

      def create
        airline = Airline.new(airline_params)
        
        if airline.save
          render json: AirlineSerializer.new(airline).serialized_json
        else
          render json: { error: airline.errors.messages }, status: 422
        end
      end

      def update
        airline = Airline.find_by(slug: params[:slug])

        if airline.update(airline_params)
          render json: AirlineSerializer.new(airline).serialized_json
        else
          render json: { error: airline.errors.messages }, status: 422
        end
      end

      def destroy
        airline = Airline.find_by(slug: params[:slug])

        if airline.destroy
          head :no_content
        else
          render json: { errors: airline.errors }, status: 422
        end
      end

      private

      def airline_params
        params.require(:airline).permit(:name, :image_url)
      end
    end
  end
end

Compound Documents

When we render data on our airline(s) using our AirlineSerializer, I want to make sure that we are also including any associated review data in that payload. One way that we can do this with fast_jsonapi is by structuring our response as a "compound document". To do this, when we initialize a new instance of our serializer, we can pass in an optional options hash and specify the resources we want to include.

So, in our airlines controller, I'm going to create a private options method where we can specify the additional resource(s) we would like to include, which at this point will just be reviews:

filepath: app/controllers/api/v1/airlines_controller.rb

module Api
  module V1
    class AirlinesController < ApplicationController

      ...

      private

      ...
      
      def options
        @options ||= { include: %i[reviews] }
      end
    end
  end
end

Now we can modify our serializers so that we are passing in a second options argument when we initialize a new instance of AirlineSerializer, for example:

AirlineSerializer.new(airline, options).serialized_json

So once we make this modification, our final airlines controller should look something like this:

filepath: app/controllers/api/v1/airlines_controller.rb

module Api
  module V1
    class AirlinesController < ApplicationController
      def index
        airlines = Airline.all

        render json: AirlineSerializer.new(airlines, options).serialized_json
      end

      def show
        airline = Airline.find_by(slug: params[:slug])

        render json: AirlineSerializer.new(airline, options).serialized_json
      end

      def create
        airline = Airline.new(airline_params)
        
        if airline.save
          render json: AirlineSerializer.new(airline).serialized_json
        else
          render json: { error: airline.errors.messages }, status: 422
        end
      end

      def update
        airline = Airline.find_by(slug: params[:slug])

        if airline.update(airline_params)
          render json: AirlineSerializer.new(airline, options).serialized_json
        else
          render json: { error: airline.errors.messages }, status: 422
        end
      end

      def destroy
        airline = Airline.find_by(slug: params[:slug])

        if airline.destroy
          head :no_content
        else
          render json: { errors: airline.errors }, status: 422
        end
      end

      private

      def airline_params
        params.require(:airline).permit(:name, :image_url)
      end

      def options
        @options ||= { include: %i[reviews] }
      end
    end
  end
end

Reviews Controller

We also need to add a controller for reviews. This will be simpler, only requiring create and destroy methods for now. So let's start by creating a new controller located at api/v1/reviews_controller.rb:

filepath: app/controllers/api/v1/reviews_controller.rb

module Api
  module V1
    class ReviewsController < ApplicationController
    end
  end
end

Reviews#create

Our create method here will be similar to the create method we built for our previous controller. We will again create a whitelist using strong parameters, this time allowing title, description, score, and airline_id as our attributes.

filepath: app/controllers/api/v1/reviews_controller.rb

module Api
  module V1
    class ReviewsController < ApplicationController

      private 

      def review_params
        params.require(:review).permit(:title, :description, :score, :airline_id)
      end
    end
  end
end

Then, we can go ahead and create a new review by initializing a new instance of our Review model, passing in our review_params. If everything looks good when we try to save the new record, we will return a new instance of our ReviewSerializer. Otherwise, we will return an error.

filepath: app/controllers/api/v1/reviews_controller.rb

module Api
  module V1
    class ReviewsController < ApplicationController
      def create
        review = Review.new(review_params)

        if review.save
          render json: ReviewSerializer.new(review).serialized_json
        else
          render json: { errors: review.errors.messages }, status: 422
        end
      end

      private 

      def review_params
        params.require(:review).permit(:title, :description, :score, :airline_id)
      end
    end
  end
end

Reviews#destroy

For our destroy method, similar to our airlines controller, we will simply find the record, and attempt to destroy it. If everything is successful, we will return an empty response with an indication that the deletion was successful. Otherwise, we will return an error.

filepath: app/controllers/api/v1/reviews_controller.rb

module Api
  module V1
    class ReviewsController < ApplicationController

      ... 
      
      def destroy
        review = Review.find(params[:id])

        if review.destroy
          head :no_content
        else
          render json: { errors: review.errors.messages }, status: 422
        end
      end

      ...

    end
  end
end

So then our full reviews controller should look something like this now:

filepath: app/controllers/api/v1/reviews_controller.rb

module Api
  module V1
    class ReviewsController < ApplicationController
      def create
        review = Review.new(review_params)

        if review.save
          render json: ReviewSerializer.new(review).serialized_json
        else
          render json: { errors: review.errors.messages }, status: 422
        end
      end
      
      def destroy
        review = Review.find(params[:id])

        if review.destroy
          head :no_content
        else
          render json: { errors: review.errors.messages }, status: 422
        end
      end

      private

      def review_params
        params.require(:review).permit(:title, :description, :score, :airline_id)
      end
    end
  end
end

Testing Our Rails API

Now that we have our models, controllers, serializers and routes all set up, let's go ahead and test out our API. We can do this using a REST client tool like Insomnia or Postman (in my examples I'll be using Insomnia).

If you need a quick refresh on what our routes look like at this point, you can do so by typing rails routes into your terminal:

terminal

                   Prefix Verb   URI Pattern                                                                              Controller#Action
                     root GET    /                                                                                        pages#index
          api_v1_airlines GET    /api/v1/airlines(.:format)                                                               api/v1/airlines#index
                          POST   /api/v1/airlines(.:format)                                                               api/v1/airlines#create
       new_api_v1_airline GET    /api/v1/airlines/new(.:format)                                                           api/v1/airlines#new
      edit_api_v1_airline GET    /api/v1/airlines/:slug/edit(.:format)                                                    api/v1/airlines#edit
           api_v1_airline GET    /api/v1/airlines/:slug(.:format)                                                         api/v1/airlines#show
                          PATCH  /api/v1/airlines/:slug(.:format)                                                         api/v1/airlines#update
                          PUT    /api/v1/airlines/:slug(.:format)                                                         api/v1/airlines#update
                          DELETE /api/v1/airlines/:slug(.:format)                                                         api/v1/airlines#destroy
           api_v1_reviews POST   /api/v1/reviews(.:format)                                                                api/v1/reviews#create
            api_v1_review DELETE /api/v1/reviews/:id(.:format)                                                            api/v1/reviews#destroy
                          GET    /*path(.:format)                                                                         pages#index

Airlines#index

First, let's check our Airlines#index endpoint. We can test this out simply by making a GET request to /api/v1/airlines.json. If you haven't already, fire up your rails server using rails s from your terminal and then go ahead and try to make a get request to this endpoint.

Airlines Index Insomnia

Looking good!

Airlines#show

Next, let's check out our Airlines#show endpoint.

Remember, we are using unique slugs for the primary param for our airlines. So, we should be able to make a GET request to /api/v1/airlines/united-airlines.json for example to get that specific airline:

Airline Show Insomnia

Looking good as well!

Airlines#create

Next let's try to create a new airline. We can do this by making a POST request to /api/v1/airlines.json. We'll need to provide a body with at least a name value as well. So let's add a JSON body to our request that has a key of name and a value of 'fake airline': { "name": "fake airline" }. Go ahead and try posting that and see what happens:

Insomnia CSRF Error

This time, our request will fail and we will get an invalid csrf token error with a response code of 422 unprocessable entity. This is because we are trying to make a post request without a valid csrf token, which rails is protecting against by default.

We can resolve this for now by going back into our airlines controller and adding protect_from_forgery with: :null_session at the top of our class:

filepath: app/controllers/airlines_controller.rb

module Api
  module V1
    class AirlinesController < ApplicationController
      protect_from_forgery with: :null_session

      ...

    end
  end
end
H/T to James Hibbard's post and this post by Marc Gauthier for this tip. Also be sure to check out these stack overflow threads on this topic.

We will also need to do this for our reviews controller:

filepath: app/controllers/reviews_controller.rb

module Api
  module V1
    class ReviewsController < ApplicationController
      protect_from_forgery with: :null_session

      ...

    end
  end
end

Now if we again try to create a new airline, everything should work correctly:

Insomnia Create Airline

Additionally, if we want to, we can now navigate to our new fake airline by it's slug (fake-airline) and view that:

Insomnia Show Fake Airline

Airlines#update

We should additionally be able to update our new airline at this point. Let's test this by making a PATCH request to /api/v1/airlines/fake-airline.json. Our request body will update the name of our airline from fake airline to other fake airline, so we can add a JSON body to our request that looks like this:

{ "name": "other fake airline" }

Insomnia Show Airline Example

Airlines#destroy

And additionally, we should see that deleting our new airline works as well, if we make a delete request to /api/v1/airlines/fake-airline.json:

Insomnia Delete Airline Example

Reviews#create

Let's also test out our reviews endpoints. We should now be able to create a new review for an airline, providing a title, description, score, as well as the airline_id for the airline we would like this review to be for. So my JSON payload to test this will look like this:


{
"airline_id": 1,
"title": "Amazing experience!",
"description": "I loved this airline. Great wifi, good snacks, friendly airline staff!",
"score": 4
}

We can create a new review by making a POST request with this payload to /api/v1/reviews.json

Insomnia Create Review

Airlines#show with reviews

Now that we have created a review for one of our airlines, we should be able to see that review in our api response when we make a request to get that airline. Let's test this out by making a request to /api/v1/airlines/united-airlines:

insomnia airlines index example with reviews

If we look now, when we make a get request to get an airline that has reviews (united airlines in this case), we can see that we have an additional "included" parameter in our json that contains an array of all of the reviews for this airline:


{
  "data": {
    "id": "1",
    "type": "airline",
    "attributes": {
      "name": "United Airlines",
      "slug": "united-airlines",
      "image_url": null
    },
    "relationships": {
      "reviews": {
        "data": [
          {
            "id": "3",
            "type": "review"
          }
        ]
      }
    }
  },
  "included": [
    {
      "id": "3",
      "type": "review",
      "attributes": {
        "title": "Amazing experience!",
        "description": "I loved this airline. Great wifi, good snacks, friendly airline staff!",
        "score": 4,
        "airline_id": 1
      }
    }
  ]
}

At this point, it looks like all of the core functionality of our backend rails api is working correctly! So with that, I think we are ready to move to the frontend and start building out the react portion of our app.

Continue to part 2: Building the react portion of our app