How To Create A Ruby On Rails API With Scaffold - Full 10 Step Guide In 5 Minutes
Elliot Mangini
Posted on October 19, 2022
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
$ npm install --prefix client
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
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
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
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
5) Seed
$ rails db:migrate db:seed
6) routes.rb - Fill it out correctly
If all routes are required we use
resources :model
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]
~) 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)
…becomes . . .
params.permit(:attr1, :attr2)
…to be safe we can also add to the TOP of each of these controllers . . .
wrap_parameters format: []
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
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
…becomes . . .
def create
@model = Model.create!(model_params)
render json: @model, status: :created
end
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
…becomes as simple as . . .
def update
@model.update!(model_params)
render json: @model, status: :accepted
end
Add head :no_content to destroy routes
def destroy
@activity.destroy
end
…becomes . . .
def destroy
@activity.destroy
head :no_content
end
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
…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
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
…becomes . . .
def create
@signup = Signup.create!(signup_params)
render json: @signup.activity, status: :created
end
@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
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
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
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
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]
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
Now, our
$ rails db:seed
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
Posted on October 19, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
October 19, 2022