DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Cover image for How To Create A Ruby On Rails API With Scaffold - Full 10 Step Guide In 5 Minutes
Elliot Mangini
Elliot Mangini

Posted on

How To Create A Ruby On Rails API With Scaffold - Full 10 Step Guide In 5 Minutes

This guide is multi-use, designed for passing a code challenge but could be useful for jumping into a new codebase, or refreshing your knowledge of ruby on rails, or getting a brief overview of how an MVC (Model View Controller) framework works!

1) Commands

$ bundle install
Enter fullscreen mode Exit fullscreen mode
$ npm install --prefix client
Enter fullscreen mode Exit fullscreen mode

2) Generators

$ rails g scaffold lowercase_snake_formatted_name attr_default_string attr_of_type:datatype model_that_has_many_of_these:belongs_to --no-test-framework
Enter fullscreen mode Exit fullscreen mode

Lowercase snake is is the only format that will turn test_model into TestModel, TestModels, has_many :test_models, etc in all the right places.

Pay attention to using belongs_to relationships in this generator as it will create the foreign key tables. We won’t need to use has_many in this line ever because of the nature of where foreign keys live.

If you make a mistake . . .

$ rails destroy scaffold scaffold_to_destroy
Enter fullscreen mode Exit fullscreen mode

3) Fill out model relationships

belongs_to will be created automatically
has_many will be created like so . . .

    has_many :signups, dependent: :destroy
    has_many :campers, through: :signups
Enter fullscreen mode Exit fullscreen mode

This is a good time to consider dependent: :destroy if applicable.

4) Fill out model validations

Here are some common ones . . .

    validates :name, presence: true
    validates :age, :inclusion => 8..18
    validates :email, :uniqueness: true
Enter fullscreen mode Exit fullscreen mode

5) Seed

$ rails db:migrate db:seed
Enter fullscreen mode Exit fullscreen mode

6) routes.rb - Fill it out correctly

If all routes are required we use

resources :model
Enter fullscreen mode Exit fullscreen mode

Otherwise the following paths correspond to the following only: array symbols
GET /models => [ . . . , :index]
GET /models/:id => [ . . . , :show]
POST /models => [ . . . , :create]
PATCH /models/:id => [ . . . , :update]
DELETE /models/:id => [ . . . , :destroy]

Altogether we end up with something like . . .

resources :models, only: [:index, :show, :create]
Enter fullscreen mode Exit fullscreen mode

~) As you do the following be mindful of what routes you need, or perhaps do ALL of the steps if unsure . . .

7) Clean Up Tasks

In any controllers that take params . . .

params.require(:model).permit(:attr1, :attr2)
Enter fullscreen mode Exit fullscreen mode

…becomes . . .

params.permit(:attr1, :attr2)
Enter fullscreen mode Exit fullscreen mode

…to be safe we can also add to the TOP of each of these controllers . . .

wrap_parameters format: []
Enter fullscreen mode Exit fullscreen mode

We can also deal with RecordNotFound errors that will be triggered by any of the above methods a single time in application_controller.rb
We add the following . . .

  rescue_from ActiveRecord::RecordNotFound, with: :render_not_found_response

  private

  def render_not_found_response(exception)
    render json: { error: "#{exception.model} not found" }, status: :not_found
  end
Enter fullscreen mode Exit fullscreen mode

In Controllers, add bang operator ! to create and update methods if they are going to be used.

def create
  @model = Model.new(model_params)

  if @model.save
    render json: @model, status: :created, location: @model
  else
    render json: @model.errors, status: :unprocessable_entity
  end
end
Enter fullscreen mode Exit fullscreen mode

…becomes . . .

def create
  @model = Model.create!(model_params)
  render json: @model, status: :created
end
Enter fullscreen mode Exit fullscreen mode

Likewise the update methods are adjusted the same way.

def update
  if @model.update(model_params)
    render json: @model
  else
    render json: @model.errors, status: :unprocessable_entity
  end
end
Enter fullscreen mode Exit fullscreen mode

…becomes as simple as . . .

def update
  @model.update!(model_params)
  render json: @model, status: :accepted
end
Enter fullscreen mode Exit fullscreen mode

Add head :no_content to destroy routes

def destroy
  @activity.destroy
end
Enter fullscreen mode Exit fullscreen mode

…becomes . . .

def destroy
  @activity.destroy
  head :no_content
end
Enter fullscreen mode Exit fullscreen mode

8) Add unprocessable_entity errors where needed

If we have a POST or UPDATE path in use for a model (create or update methods) we also must add to our private methods inside the controller PRIVATE section . . .

def render_unprocessable_entity_response(invalid)
  render json: { errors: invalid.record.errors.full_messages }, status: :unprocessable_entity
end
Enter fullscreen mode Exit fullscreen mode

…since these methods take parameters (strong) and at the TOP of this controller we must add . . .

rescue_from ActiveRecord::RecordInvalid, with: :render_unprocessable_entity_response
Enter fullscreen mode Exit fullscreen mode

This must be in place in each controller where POST or UPDATE are in use.

9) Special Concerns

The β€œInstead Of” case: When a user signs up for a signup instead of returning the signup we want to return what they signed up for. This is very easy we do the following for example . . .

def create
  @signup = Signup.create!(signup_params)
  render json: @signup, status: :created
end
Enter fullscreen mode Exit fullscreen mode

…becomes . . .

def create
  @signup = Signup.create!(signup_params)
  render json: @signup.activity, status: :created
end
Enter fullscreen mode Exit fullscreen mode

@signup => @signup.activity

We can also solve this by creating a new serializer, but we don’t always need to. If it’s a wickedly simple replace the instance with an associated instance that is formatted using the default serializer we can do it like the above.

In other words we are returning the default-formatted associated model instance of interest instead of the one being operated on.

Return All Associated Instances case: I have a user with many tickets, when I show method/route on this user I want their tickets to be returned as children. As long as model relationships and db are associated properly this is as simple as adding to the UserSerializer the following line . . .
has_many :tickets

The tickets will be formatted using the default TicketSerializer.

It’s good to note here that if we have a many-to-many relationship like a doctor who has many patients through appointments we can return the patients the same way using directly a has_many :patients and the serializer should know to jump over the join table.

But Only Sometimes case: What if I don’t want the tickets to come through on an index method/route when we see all the users and instead only when we see just one? Instead of modifying the main serializer, in this case UserSerializer we can make a new one . . .

$ rails g serializer user_show_tickets
Enter fullscreen mode Exit fullscreen mode

Since this serializer wasn’t scaffolded we need to make sure we add the appropriate main attributes at the top. They can be copied from the regular UserSerializer as long as we want them . . .

attributes :id, :name, :age, :email
Enter fullscreen mode Exit fullscreen mode

Then we add the has_many here instead of in the UserSerializer. Altogether it looks something like . . .

class UserShowTicketsSerializer < ActiveModel::Serializer
  attributes :id, :name, :age, :email
  has_many :tickets
end
Enter fullscreen mode Exit fullscreen mode

Finally, we specify that this method/route uses this serializer inside the controller where it is declared. . .

def show
  render json: @user, serializer: UserShowTicketsSerializer
end
Enter fullscreen mode Exit fullscreen mode

We can specify a serializer to do anything on a specific route this way, however it’s important to note that inside a serializer we have access to the instance by calling it object if we want to access specific properties of it.

-----------------------------------------------

10) Troubleshooting

If we are using @model instance methods or a set/find private method throughout our controllers this is a good clue that we should include a line like the following at the top of the controller if we want to use that variable directly. Scaffolding takes care of this automatically, but in case it’s helpful . . .

before_action :set_instance, only: [:show, :update, :destroy]
Enter fullscreen mode Exit fullscreen mode

Here we are saying we need to set this variable when we use the following method-routes (the ones that depend on using that variable).

If we are using the server and npm to test instead of using tests it might become critical to clear our database on re-testing in case bad data was passed into the db along the way of getting our code working . . . we can add the following to our seeds.rb . . . (above the create invocations)

puts "Clearing database"
ModelOne.destroy_all
ModelTwo.destroy_all
ModelThree.destroy_all
Enter fullscreen mode Exit fullscreen mode

Now, our

$ rails db:seed
Enter fullscreen mode Exit fullscreen mode

works a lot like a replant.

BONUS) Comment out or remove routes in the controllers that aren’t being used.

Hope this is helpful, let me know if I missed anything or made errors!
-Elliot/Big Sis

Top comments (2)

Collapse
 
f53 profile image
F53

good shit

Collapse
 
elliotmangini profile image
Elliot Mangini

thanks chase πŸ™Œ

We are hiring! Do you want to be our Senior Platform Engineer? Forem is hiring a Senior Platform Engineer

If you're interested in ops and site reliability and capable of dipping in to our Linux stack, we'd love your help shoring up our systems!