Simple, beginner steps: API with Rails!

aprilskrine

April Skrine

Posted on March 18, 2022

Simple, beginner steps: API with Rails!

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

In Sitter:

has_many :pets
has_many :owners, through: :pets
Enter fullscreen mode Exit fullscreen mode

Now our relationships are confirmed. In the Pets model you should see:

belongs_to :owner
belongs_to :sitter
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

In Sitter model:

validates :age, numericality: {greater_than_or_equal_to: 18}
Enter fullscreen mode Exit fullscreen mode

In Pet model:

validates :name, presence: true
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

  1. 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
Enter fullscreen mode Exit fullscreen mode
  1. 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
Enter fullscreen mode Exit fullscreen mode

If we call that custom serializer in our show method:

def show
     render json: Owner.find(params[:id]), serializer: OwnerIdSerializer, status: :ok

end

Enter fullscreen mode Exit fullscreen mode
💖 💪 🙅 🚩
aprilskrine
April Skrine

Posted on March 18, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related