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
Top comments (0)