DEV Community

Jean Aguilar
Jean Aguilar

Posted on • Originally published at niceguysfinishlast.dev

Rails API with a frontend built in React, Part III.

In this post we will be adding a Movie resource to our API. Let's modify our routes file in config/routes.rb

Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      resources :movies
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

If we run rails routes we should be able to see something like this

Movie routes

Lets create a new controller to be the one to do our API stuff.

$ mkdir app/controllers/api
$ mkdir app/controllers/api/v1
$ touch app/controllers/api/v1/api_controller.rb
Enter fullscreen mode Exit fullscreen mode

First we created two new directories for namespacing our API, it is important to do it if we are planning to version the API and also if we plan future releases. We created a new controller called ApiController which is the one we will be inheriting from, just leave it like this. Namespacing is very important in order for this to work.

class Api::V1::ApiController < ActionController::API
end
Enter fullscreen mode Exit fullscreen mode

 Movie model.

Now we're going to create our first model, the Movie model, our movie will have a title, a plot and a release date, at least for now.

$ rails generate model Movie title plot:string release_date:date
Enter fullscreen mode Exit fullscreen mode

It should generate four new files, one containing the migration, two for our tests and the file for the model.

Movie Model files

Now let's create our database and then apply the new migration.

$ rails db:create
$ rails db:migrate
Enter fullscreen mode Exit fullscreen mode

After that we need to add some validations, the title and the date must be required fields.

class Movie < ApplicationRecord

  validates :title, :release_date, presence: true

end
Enter fullscreen mode Exit fullscreen mode

Testing models with shoulda matchers.

For testing the validations we just made, we're going to be using shoulda matchers, one of the gems added in the last tutorial(one liners tests for rails), by modifying our newly generated file spec/models/movie_spec.rb

RSpec.describe Movie, type: :model do
  describe "validations" do
    it { is_expected.to validate_presence_of(:title) }
    it { is_expected.to validate_presence_of(:release_date) }
  end
end
Enter fullscreen mode Exit fullscreen mode

Notice how in the it statements we added a describe block with two lines for validating the presence of our fields, that's how shoulda matchers works, it lets you write a simple readable one line to test the functionality of your rails app.

Creating our first factory.

Now that we have that, we need to modify the other test file that was generated by us when we run the rails generate model, this is a factory file, and we're going to modify it a bit. Thats when faker comes in handy, because it lets us fill the factory with dummy data in order to be used in the tests, in this way we can create tests using different data values, it is important to be careful though, because faker does not have unlimited data values, so there's a possibility to create duplicate values, and if one of our models validates the uniqueness of a certain field, the test can break because there's already a resource with that name; Anyways that's how the file should look like(There's no movie names in faker gem, so I'm using Book names instead).

FactoryBot.define do
  factory :movie do
    title { Faker::Book.title }
    plot { Faker::Lorem.paragraph }
    release_date { Faker::Date.birthday(18, 65) }
  end
end
Enter fullscreen mode Exit fullscreen mode

Seeding the database.

Now that you see that faker can generate fake data, let's create some seeds for our movies in db/seeds.rb

10.times do
  Movie.create(
    title: Faker::Book.title,
    plot: Faker::Lorem.paragraph,
    release_date: Faker::Date.birthday(18, 65),
  )
end
Enter fullscreen mode Exit fullscreen mode

Finally run this command to add the fake movies

$ rails db:seed
Enter fullscreen mode Exit fullscreen mode

Creating the movie controller.

Great, now its time to create a controller for our Movie model, we're going to add a new file called movies_controller.rb in our api namespace.

$ touch app/controllers/api/v1/movies_controller.rb
Enter fullscreen mode Exit fullscreen mode

This controller will be on charge of displaying, editing, adding and deleting movie resources, for tutorial purposes I will not explain how a regular controller works, I'm just goingt to show you the actual code.

class Api::V1::MoviesController < Api::V1::ApiController
  before_action :set_movie, only: %i[show update destroy]

  def index
    @movies = Movie.all

    render json: @movies
  end

  def show
    render json: @movie
  end

  def create
    @movie = Movie.new(movie_params)

    if @movie.save
      render json: @movie, status: :created
    else
      render json: @movie.errors, status: :unprocessable_entity
    end
  end

  def update
    if @movie.update(movie_params)
      render json: @movie
    else
      render json: @movie.errors, status: :unprocessable_entity
    end
  end

  def destroy
    @movie.destroy
  end

  private
    def set_movie
      @movie = Movie.find(params[:id])
    end

    def movie_params
      params.require(:movie).permit(:title, :plot, :release_date)
    end
end
Enter fullscreen mode Exit fullscreen mode

If you're familiar with the framework this code should not surprise you, and is this your first rails API you can see that we're rendering JSON instead of a regular html view, the rest is just a regular controller logic flow.

Serializers

In order to display that we are going to use a gem called active_model_serializers, which is a gem that is going to allow us to modify which content should be displayed in our JSON responses.

# Rest of the gems
gem "active_model_serializers", "~> 0.10.0"
Enter fullscreen mode Exit fullscreen mode

After bundling that new gem, we're going to create a new serializer for our Movie resource, but first lets see how a regular response looks at the moment, we can check that using postman, curl or whatever you want, just start your rails server. And try to make a get request to this URL: http://localhost:3000/api/v1/movies/1 and your output should be similar to the following:

{
  "id": 1,
  "title": "In Death Ground",
  "plot": "Maxime nulla architecto. Sit et et. Et vero reiciendis.",
  "release_date": "1979-03-29",
  "created_at": "2019-06-07T04:22:30.850Z",
  "updated_at": "2019-06-07T04:22:30.850Z"
}
Enter fullscreen mode Exit fullscreen mode

Notice that we're displaying everything in the response, this is not a good idea because there can be some cases where we want to hide certain information in our response. Because of that we're using serializers, it is worth to mention that sometimes people prefer to use Jbuilder which basically is creating instead of an html.erb file one with an extension of json.jbuilder under the views folder. While this approach is also a good one, I prefer to use the serializers because I don't like the idea of creating files in the views folder. If we're making an API, sounds kind of dumb, even though it is a completely valid approach, for me it does not make sense to use the views folder in an API only application.

Lets create our serializer file.

$ mkdir app/serializers
$ touch app/serializers/movie_serializer.rb
Enter fullscreen mode Exit fullscreen mode

Now lets edit our newly created file

class MovieSerializer < ActiveModel::Serializer

  attributes :id, :title, :plot, :release_date

end
Enter fullscreen mode Exit fullscreen mode

Now if we try to perform the same get request we should get something like this:

{
  "id": 1,
  "title": "In Death Ground",
  "plot": "Maxime nulla architecto. Sit et et. Et vero reiciendis.",
  "release_date": "1979-03-29"
}
Enter fullscreen mode Exit fullscreen mode

Notice that in our new response we're omiting the created_at and the updated_at fields, because we did not specify them in our serializer.

RSpec controller test

If you want to play with the functionality of the controller actions feel free to do so, I'll skip that part to show you how to start testing the controller in the application. Lets create the test file.

$ mkdir spec/controllers
$ mkdir spec/controllers/api
$ mkdir spec/controllers/api/v1
$ touch spec/controllers/api/v1/movies_controller_spec.rb
Enter fullscreen mode Exit fullscreen mode

Now the basic tests controller structure is this one(notice the namespacing like in the controller)

require "rails_helper"

RSpec.describe Api::V1::MoviesController, type: :controller do
  # Your tests
end
Enter fullscreen mode Exit fullscreen mode

This is the basic structure, normally when you test controllers you need to ensure that all the actions are tested, lets start defining attributes for our object, this will allow to test when we're performing POST or PUT actions.

RSpec.describe Api::V1::MoviesController, type: :controller do
  let(:valid_attributes) do
    {
      title: "American Pie",
      plot: "Teen Comedy",
      release_date: "09-06-1999",
    }
  end

  let(:invalid_attributes) do
    { release_date: nil }
  end
end
Enter fullscreen mode Exit fullscreen mode

Here we're instantiating two variables to be available to use in the describe blocks of our actions, these are working la global variables for this file.

Now lets start writing some actions, first we're going to add the index and show actions.

RSpec.describe Api::V1::MoviesController, type: :controller do
  # Rest of the code
  describe "GET #index" do
    it "returns a success response" do
      get :index, params: {}
      expect(response).to be_successful
    end
  end

  describe "GET #show" do
    it "returns a success response" do
      movie = create(:movie)
      get :show, params: { id: movie.to_param }
      expect(response).to be_successful
    end
  end
  # Rest of the code
end
Enter fullscreen mode Exit fullscreen mode

In this two describe blocks we're performing a get request to our index and show actions, the index does not require any params to work so we're not sending anything, the show action by counterpart does need one param, which we in rails by default is the :id. We're using factory bot to create a new movie and the use the new id of that movie to pass it in our params.

For our create action the code should look like this.

RSpec.describe Api::V1::MoviesController, type: :controller do
  # Rest of the code
  describe "POST #create" do
    context "with valid params" do
      it "creates a new Movie" do
        expect do
          post :create, params: { movie: valid_attributes }
        end.to change(Movie, :count).by(1)
      end

      it "returns a 201 status code" do

        post :create, params: { movie: valid_attributes }
        expect(response).to have_http_status(:created)
      end
    end

    context "with invalid params" do
      it "does not create a new Movie" do
        expect do
          post :create, params: { movie: invalid_attributes }
        end.to change(Movie, :count).by(0)
      end

      it "returns a 422 status code" do

        post :create, params: { movie: invalid_attributes }
        expect(response).to have_http_status(:unprocessable_entity)
      end
    end
  end
  # Rest of the code
end
Enter fullscreen mode Exit fullscreen mode

Notice that in our describe we are using two contexts, one with valid attributes and the other with invalid attributes. The two contexts are basically testing the same, with the valid attributes we're expecting two things, one is to persist a new movie in our database, so that will change our Movie count by 1 and also we expect a 201 created response from the server, because a new movie was created. The other context is doing the opposite, it is expecting the invalid attributes to not change our Movie count, and to return an unprocessable entity response to tell users that the movie could not be created.

To continue with this test we are going to add the update action.

RSpec.describe Api::V1::MoviesController, type: :controller do
  # Rest of the code
  describe "PUT #update" do
    context "with valid params" do
      let(:new_attributes) do
        {
          title: "American Pie 2",
          release_date: "06-08-2001",
        }
      end

      it "updates the requested movie" do
        movie = create(:movie)
        put :update, params: { id: movie.to_param, movie: new_attributes }
        movie.reload
        expect(movie.attributes).to include("title" => "American Pie 2")
      end

      it "returns a 200 status code" do
        movie = create(:movie)

        put :update, params: { id: movie.to_param, movie: valid_attributes }
        expect(response).to have_http_status(:ok)
      end
    end

    context "with invalid params" do
      it "returns a 422 status code" do
        movie = create(:movie)

        put :update, params: { id: movie.to_param, movie: invalid_attributes }
        expect(response).to have_http_status(:unprocessable_entity)
      end
    end
  end
  # Rest of the code
end
Enter fullscreen mode Exit fullscreen mode

The update action is different, because we're changing how the attributes look, so here we need to instance a let variable to our pass the new attributes, and then we're creating two context like in our create action to validate the two possible scenarios, if the movie updates correctly we should expect the updated attributes to be equal as the new_attributes variable and we should also expect a 200 ok response from the server to tell the user that the change was succesful. In our failure context we are testint that if we sent a nil value to one of our required fields, the update will fail, because we defined that the release_date can't be blank.

Our final action to test is called destroy

RSpec.describe Api::V1::MoviesController, type: :controller do
  # Rest of the code
  describe "DELETE #destroy" do
    it "destroys the requested movie" do
      movie = create(:movie)
      expect do
        delete :destroy, params: { id: movie.to_param }
      end.to change(Movie, :count).by(-1)
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

The describe works as the opposite as the create, because we're expecting the movie count to be minus one because a movie was deleted. Notice how the factory helps to avoid code duplication.

Cross-origin resource sharing.

Now that everything is tested we're done with our movie resource, now its time to allow our API to receive requests from external sources, for example our local frontend, running in another port. We need to install the gem which is commented in the gemfile.

gem "rack-cors"
Enter fullscreen mode Exit fullscreen mode

After running bundle install lets modify the cors file:

# config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins "*"

    resource "*",
             headers: :any,
             methods: %i[get post put patch delete options head]
  end
end
Enter fullscreen mode Exit fullscreen mode

This should be configured accordingly and my example here should not be used in a real API, because basically we're allowing any origin to access and perform any request to our API.

To end this tutorial and before making any push to a remote repo we should check if rspec and rubocop are passing, let's try that.

$ rubocop -a
Enter fullscreen mode Exit fullscreen mode

Which should produce something like this:

Correcting Offenses

And now lets run RSpec to see if our tests are passing

$ rspec
Enter fullscreen mode Exit fullscreen mode

And you should see everything passing.

RSpec tests

Yei, that should be everything for this post, feel free to correct me or ask if something was not clear and stay tuned for the next post, which will be integrating this to the react project.

Top comments (1)

Collapse
 
dbpatnode profile image
Daniel Patnode

Any chance you could post the entirety of the rspec test file? I'm running into some issues there and would love to see what I'm missing.