Simple, beginner steps: API with Rails!
Tags: beginners, webdev, rails, ruby
Published: 2022-03-18 18:17:34

## 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. 

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
```
2. 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

```