April Skrine
Posted on March 18, 2022
Let's talk through some basics to building out an API in Rails.
Make sure you have rails installed first by running gem install rails
! This assumes you already have a rails directory started. If not, run rails new [name] --api
to start new.
1. Build out your resources
For the sake of this example, we're going to work with the following:
- Owner --< Pets >-- Pet Sitters
- An owner has many pets
- A pet sitter has many pets they take care of
- Through relationships apply
Let's build out our resources. We're going to use rails g resource
because it will generate model, controller, migration and serializers for us.
rails g resource owner name phone:integer
rails g resource sitter name phone:integer active:boolean
rails g resource pet name age:integer sitter:references owner:references
This will build out our resources, and include the foreign keys in the pet migration. References will ensure the foreign keys cannot have a null value when we migrate and update the schema.
2. Add/confirm relationships
Once we've created our resources with the generator, let's make sure our relationships are correct. The resource generator automatically generated the belongs_to
when we used references, but we need to go into the Owner and Sitter models and include:
In Owner:
has_many :pets, dependent: :destroy
has_many :sitters, through: :pets
In Sitter:
has_many :pets
has_many :owners, through: :pets
Now our relationships are confirmed. In the Pets model you should see:
belongs_to :owner
belongs_to :sitter
This is also a good time to think about whether any relationships will need dependent: :destroy
. Obviously if an owner deletes their profile, the pets they own should be deleted as well. We included this in the Owner model.
3. Validations
Now that our models are linked, let's look at validations in our models. Let's assume an owner must be 16 or older to use our service, and a pet sitter must be 18 to be a pet sitter. Also, a pet cannot be created without a name.
In Owner model:
validates :age, numericality: {greater_than_or_equal_to: 16}
In Sitter model:
validates :age, numericality: {greater_than_or_equal_to: 18}
In Pet model:
validates :name, presence: true
These are the validations we've added in each of the models.
4. Routes
Let's take a look at our routes in Config >> Routes. Since we used the resource generator, we'll already see:
resources :owners
resources :sitters
resources :pets
This means that all default routes are currently open. Let's clean up what's accessible:
resources :owners, only: [:index, :show, :create, :destroy]
resources :sitters, only: [:index, :show, :create, :destroy]
resources :pets, only: [:index, :show]
We can define the routes any way we want to, but this is what we'll arbitrarily use for now.
5. Controllers
Now that we've got the routes, let's head for the Controllers and get some of the routes defined. Let's just look at an example of OwnersController:
def index
render json: Owner.all, status: :ok
end
def show
render json: Owner.find(params[:id]), status: :found
end
def create
render json: Owner.create!(owner_params), status: :created
end
def destroy
Owner.find(params[:id].destroy
head :no_content
end
private
def owner_params
params.permit(:name, :phone, :age)
end
Make sure each Controller has routes defined for each route we've specified in our Config >> Routes. Technically, we didn't really need strong params for this, but I wanted to show an example. In the private methods of the controller, I've defined the strong params, which are the only params that will allowed to be passed.
!!! I've used find
because it throws an error. I've also added the bang operator in the create method. In our ApplicationController, you would find something like this:
class ApplicationController < ActionController::API
include ActionController::Cookies
rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from ActiveRecord::RecordInvalid, with: :render_unprocessable_entity
private
def not_found(error)
render json: {error: "#{error.model} not found"}, status: :not_found
end
def render_unprocessable_entity(invalid)
render json: {errors: invalid.record.errors.full_messages}, status: :unprocessable_entity
end
end
All of our controllers inherit from ApplicationController, so we can write these rescues only once and they will be applicable for any of our Controllers that need them.
6. Serializers
First, you need to ensure that the serializer gem is in the Gemfile. If it's not, add gem "active_model_serializers", "~> 0.10.12"
Our resource generator already generated serializers for each model when we used the resource generator. We can use those serializers to limit what we receive in our responses. Anything in the related Controller will default to the serializer with the matching name, unless you specify otherwise. In our OwnerController, that would look like:
render json: <something>, serializer: <CustomSerializerName>, status: :ok
If you'll be using the default serializer with the corresponding name, you can omit the serializer specification in the Controller. However, let's say we have two serializers for Owners.
- One default for
index
, so we see all owners with their :id, :name, :age, :phone like so:
class OwnerSerializer < ActiveModel::Serializer
attributes :id, :name, :age, :phone
- One custom serializer for
show
, so when we access individual owner info their pets are also returned:
class OwnerIdSerializer < ActiveModel::Serializer
attributes :id, :name, :age, :phone
has_many :pets
If we call that custom serializer in our show
method:
def show
render json: Owner.find(params[:id]), serializer: OwnerIdSerializer, status: :ok
end
Posted on March 18, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.