Building a Rails API for React, Part III: Building the Movie Resource

June 7, 2019 · 4 min read

Update — April 4, 2026: This post has been updated to improve clarity and structure. Key changes include enhanced explanations for API namespacing, refined code block formatting, and more detailed guidance on using Serializers versus Jbuilder.

In the third part of this series, we shift our focus back to the Rails backend to implement our first core resource: Movies. We'll ensure our architecture is ready for scale by using proper namespacing and a robust testing suite.

More on this series:

1. Namespaced Routing

To keep our API versioned and clean, we wrap our resources in api/v1 namespaces. This prevents breaking changes for existing users when we decide to release a version 2.

Edit config/routes.rb:

Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      resources :movies
    end
  end
end

Run rails routes to verify your endpoints:

Movie routes

2. The API Base Controller

Inheriting from ActionController::API instead of Base keeps our controllers lightweight by skipping middleware intended for HTML rendering.

$ mkdir -p app/controllers/api/v1
$ touch app/controllers/api/v1/api_controller.rb

Define the base class:

class Api::V1::ApiController < ActionController::API
end

3. The Movie Model

Our initial Movie schema will include a title, plot, and release date.

$ rails generate model Movie title plot:string release_date:date
$ rails db:create
$ rails db:migrate

Movie Model files

Model Validations

We need to ensure the title and release_date are always provided.

class Movie < ApplicationRecord
  validates :title, :release_date, presence: true
end

Testing with Shoulda Matchers

Using the gems we added in Part I, we can write expressive, one-liner tests in 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

4. Factories and Seeding

To avoid repetitive setup in our tests, we use FactoryBot combined with Faker for dynamic dummy data.

Creating the Factory

# spec/factories/movies.rb
FactoryBot.define do
  factory :movie do
    title { Faker::Book.title }
    plot { Faker::Lorem.paragraph }
    release_date { Faker::Date.birthday(18, 65) }
  end
end

Seeding the Database

Populate your development environment with 10 random 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

Run $ rails db:seed to apply.


5. The Movies Controller

This controller handles our standard CRUD operations, rendering JSON directly instead of HTML views.

# app/controllers/api/v1/movies_controller.rb
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

  # ... update and destroy actions
end

6. Controlling JSON with Serializers

By default, Rails renders all model attributes, including sensitive or unnecessary fields like created_at. We use Active Model Serializers to prune our responses.

Add to Gemfile:

gem "active_model_serializers", "~> 0.10.0"

Defining the Serializer

# app/serializers/movie_serializer.rb
class MovieSerializer < ActiveModel::Serializer
  attributes :id, :title, :plot, :release_date
end

Now, a GET request to /api/v1/movies/1 will return only the fields defined above, keeping your API payload lean and secure.


7. Enabling CORS

Since our React frontend and Rails API run on different ports (usually 3001 and 3000), browsers will block requests by default. We must enable Cross-Origin Resource Sharing.

Add gem "rack-cors" to your Gemfile and configure the initializer:

# config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins "*" # In production, replace this with your specific frontend domain
    resource "*",
             headers: :any,
             methods: %i[get post put patch delete options head]
  end
end

Security Note: Never use origins "*" in a production environment. Always whitelist specific domains to prevent unauthorized access.


Verification

Before wrapping up, always ensure your code adheres to standards and passes all tests.

$ rubocop -a
$ rspec

Correcting Offenses RSpec Passed

Everything is green! In the next post, we'll head back to the React side to start consuming these endpoints.