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.
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:
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
- Creating Our Models
- Seeding Our Database
- Serializers: Building Our JSON API
- Controllers
- Testing Out Our API With An HTTP Client (Insomnia)
- Testing Our Airlines Endpoints
- Airlines#index
- Airlines#show
- Airlines#create
- Handling CSRF Errors <- Important
- Airlines#update
- Airlines#destroy
- Testing Our Reviews Endpoints
- Testing Airlines#show with reviews
- Building Our React Front End
- The Javascript Pack Tag
- Creating A Components Folder
- React Router/React Router Dom
- BrowserRouter, Router, Route, & Switch
- Building Out Our Airlines#index View
- Creating Our Page Layout
- Making API requests to our rails api with axios
- Creating an Airline Grid Component
- Props
- React Router & Link
- Disabling Turbolinks <- Important
- Styled Components
- Styling Our Grid
- Building our Airlines#show View
- Building our Review Component
- Building our Star Rating Component
- Getting The Average Score For Our Airlines
- Building our Review Form
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
class AirlineSerializer
include FastJsonapi::ObjectSerializer
attributes :name, :slug, :image_url
end
And a reviews serializer that should look like this:
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:
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:
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
:
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:
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:
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
:
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:
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:
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.
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:
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
:
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:
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
:
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.
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.
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.
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:
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:
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.
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:
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:
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:
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:
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:
Additionally, if we want to, we can now navigate to our new fake airline by it's slug (fake-airline
) and view that:
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" }
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
:
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
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
:
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.