This week, you've begun learning about the different components of a Rails application - let's see how they all work together to produce a working application!
Honest Tom runs a used car business in Quincy, and he needs an application that will allow him to keep track of every sale he and his salespeople make. For now the front-end can be very simple, since he'll be bringing in a specialist to fine-tune the UI.
We're going to build this app together; then, in your squads, you will each build your own application based on a similar pattern!
Before we ever set hands to keyboard, the first thing we need to do is some planning. In particular, we should think about what kinds of data our application needs to store and manage.
Based on speaking with Honest Tom about his business needs, we've learned that each sale record needs to have
- The make, model, year, and condition (e.g. "excellent", "good", "fair", "damaged") of the car sold.
- The date and time of the sale.
- The name of the salesperson.
- The sale price.
Based on this description, our app only has one kind of resource, "Sales". For now, we're not sure what the front-end will need to be able to do, so for now we'll plan to make all standard CRUD behaviors available through their conventional routes; if and when we get new information from the UI specialist, we can change the app to meet their specs.
It seems that we need a table, sales
, to keep track of all of our sales. Based on what you've learned about models and Postgres over the last few days, how might we want to structure the sales
table? What columns will we need, and what types of data should each column be?
In your squads, take five minutes to discuss some options; we'll bring it back to the class for some open discussion about pros and cons.
Suppose that we came out of our planning with the following structure for our table:
Column | Type |
---|---|
id | INTEGER |
car_make | CHARACTER VARYING |
car_model | CHARACTER VARYING |
car_year | INTEGER |
car_condition | CHARACTER VARYING |
salesperson_name | CHARACTER VARYING |
sale_price | DECIMAL |
updated_at | TIMESTAMP WITHOUT TIME ZONE |
created_at | TIMESTAMP WITHOUT TIME ZONE |
Now that we've mapped out how we want our data to be structured, let's start building our back-end.
rails new sales_app_api -T --database=postgresql
rake db:create
Our app only has one resource, "Sales", so we just need one table (sales
) and one model (Sale
). Since our model will depend on the table existing, let's make the table first.
rails g migration CreateSales
will create a new migration file with the boilerplate for creating a new table in Postgres. It should be found at sales_app_api/db/migrate/YYYYMMDDHHMMSS_create_sales.rb
, and look something like this:
class CreateSales < ActiveRecord::Migration
def change
create_table :sales do |t|
end
end
end
If we fill this in with the data types we laid out earlier, we get
class CreateSales < ActiveRecord::Migration
def change
create_table :sales do |t|
t.string :car_make
t.string :car_model
t.integer :car_year
t.string :car_condition
t.string :salesperson_name
t.decimal :sale_price
t.timestamps null: false # This last one will take care of `updated_at` and `created_at`
end
end
end
Run rake db:migrate
to actually execute your migration; when it's done, take a look at schema.rb
. Does it look like we expect? What if we open up the database using rails db
? Do those tables look right?
Now that we've created our table, let's make a model so that we can easily access and manipulate the table from within Rails. Inside sales_app_api/app/models
, create a new file called sale.rb
, with the following code inside:
class Sale < ActiveRecord::Base
end
The Sale
class will inherit a number of methods from the Base
class inside the ActiveRecord
module, both private (internal methods for communicating with the database) and public-facing (such as .create!
and .all
), so we don't actually need to add any of our own code (at least, not yet) in order to get it to work.
To test out our new model, let's open up the Rails console (rails console
or rails c
) and run the following code:
s = Sale.new
s.car_make, s.car_model, s.car_year, s.car_condition = "Toyota", "Camry", 2007, "fair"
s.salesperson_name, s.sale_price = "Matt", 7000.00
If we type s
into the console, it should evaluate to:
=> #<Sale id: nil, car_make: "Toyota", car_model: "Camry", car_year: 2007, car_condition: "fair", salesperson_name: "Matt", sale_price: #<BigDecimal:7fcd94c27dd8,'0.7E4',9(36)>, created_at: nil, updated_at: nil>
Based on this, our model appears to have initialized correctly - now we just need to see if it will save to the database. If we run s.save
, we should get output like this:
(1.6ms) BEGIN
SQL (3.3ms) INSERT INTO "sales" ("car_make", "car_model", "car_year", "car_condition", "salesperson_name", "sale_price", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING "id" [["car_make", "Toyota"], ["car_model", "Camry"], ["car_year", 2007], ["car_condition", "fair"], ["salesperson_name", "Matt"], ["sale_price", "7000.0"], ["created_at", "2015-10-28 09:55:47.705470"], ["updated_at", "2015-10-28 09:55:47.705470"]]
(3.0ms) COMMIT
=> true
It looks like the change was made successfully. However, it might still be a good idea to look at the data directly using rails db
. If we open up the sales
table using `SELECT, we should now see a row like this.
id | car_make | car_model | car_year | car_condition | salesperson_name | sale_price | created_at | updated_at
---+----------+-----------+----------+---------------+------------------+------------+---------------------------+---------------------------
...| ... | ... | ... | ... | ... | ... | ... | ...
8 | Toyota | Camry | 2007 | fair | Matt | 7000.0 | 2015-10-28 09:55:47.70547 | 2015-10-28 09:55:47.70547
Great! It seems like our Sale
model is working correctly.
Now that we have a model for our resource, let's make a controller so that we can handle sale-related requests. In sales_app_api/app/controllers
, we'll create a new file called sales_controller.rb
; there, we'll define a new class called SalesController
.
class SalesController < ApplicationController
end
Next, we'll add methods to the class that correspond to the standard CRUD (create, read, update, destroy) actions.
class SalesController < ApplicationController
def index # i.e. read *all* sales
end
def show # i.e. read *one* sale
end
def create
end
def update
end
def destroy
end
end
Then, we'll write some routes for these controller methods into sales_app_api/config/routes.rb
, the configuration file for our router. We'll try to follow RESTful conventions for this.
get 'sales' => 'sales#index'
get 'sales/:id' => 'sales#show'
post 'sales' => 'sales#create'
patch 'sales/:id' => 'sales#update'
delete 'sales/:id' => 'sales#destroy'
With each request that comes in, Rails creates a hash called 'params' to hold information that's been stripped out of the request; this hash is available to all controllers so that they can operate on the data inside. By specifying
:id
here as part of those routes (in place of a value; e.g. '3' in/sales/3
), we are instructing Rails to add a new key-value pair toparams
(in this case,:id => 3
).
Coming back to our controller, we still need to fill in the bodies of each of our methods.
-
For
index
, what we want the server to send back a JSON string of all of the Sales we have on record. To do that, we might writedef index # i.e. read *all* sales render json: Sale.all end
-
For
show
, we again want the server to send back a JSON string, this time for one single Sale record. We can use theSale.find
method to look up a specific sale by its ID, using theparams
hash.def show # i.e. read *one* sale render json: Sale.find(params[:id]) end
-
Skipping down a little, we also need to look up a particular Sale record for
destroy
, so that method will look a lot likeshow
; however instead of rendering JSON, this method will simply destroy the record it finds.def destroy sale = Sale.find(params[:id]) sale.destroy end
Before we look at
create
andupdate
, let's deviate from our path for a minute and set up some strong parameters for ourSalesController
. By default, if you pass theparams
hash intoSale.create
, Rails will attempt to shoehorn every property specified into a new instance of the model. Since there is no limit to how many parameters (or what kinds of parameters) may be included within a request, this presents a big security risk. As of Rails 4, the suggested way to address this issue is through the use of the 'strong parameters' methodsrequire
andpermit
, which take your params hash and transform it into a new hash that meets certain specifications.
Strong parameters are usually incorporated into a class through a private method that can be called by both create
and update
. Here's how such a method might look for this particular controller.
private
def sale_params
params.require(:sale).permit(:car_make, :car_model, :car_year, :car_condition, :salesperson_name, :sale_price)
end
Assuming that we have a sale_params
method set up to give us 'strong params', here's how our create
and update
methods might look.
-
For
create
, we want the model to create a new instance of the model, using the strong params as an argument. If we can successfully save this new Sale to the database, we should send back some kind of positive confirmation; otherwise, we should send back the errors we get back from the database.def create sale = Sale.create(sale_params) if sale.save render json: sale # Send back the newly created Sale, as a JSON. else render json: sale.errors, status: :unprocessable_entity # Send back errors. end end
-
update
looks almost identical tocreate
; the biggest difference is that withupdate
, we need to start out by finding an element with the given particular id.def update sale = Sale.find(params[:id]) if sale.update(sale_params) sale.save render json: sale # Send back the newly updated Sale. else render json: sale.errors, status: :unprocessable_entity # Send back errors. end end
By the end, our controller should look like this.
class SalesController < ApplicationController
def index # i.e. read *all* sales
render json: Sale.all
end
def show # i.e. read *one* sale
render json: Sale.find(params[:id])
end
def create
sale = Sale.create(sale_params)
if sale.save
render json: sale # Send back the newly created Sale, as a JSON.
else
render json: sale.errors, status: :unprocessable_entity # Send back errors.
end
end
def update
sale = Sale.find(params[:id])
if sale.update(sale_params)
sale.save
render json: sale # Send back the newly updated Sale.
else
render json: sale.errors, status: :unprocessable_entity # Send back errors.
end
end
def destroy
sale = Sale.find(params[:id])
sale.destroy
end
private
def sale_params
params.require(:sale).permit(:car_make, :car_model, :car_year, :car_condition, :salesperson_name, :sale_price)
end
end
Rather than taking this on faith, let's actually fire up our server and test it! Before we do, though, we need to do a little setup.
- Go to
sales_app_api/app/controllers/application_controller.rb
and comment out the lineprotect_from_forgery with: :exception
- Add
gem 'rack-cors', :require => 'rack/cors'
to your Gemfile and runbundle install
We'll be using a tool called Postman to do our testing.
- What happens if we try to send a POST to
localhost:3000/sales
? - A PATCH to
localhost:3000/sales/1
? - What if our parameters are set up incorrectly? What happens?
If the API behaves as expected, congratulations - We've just finished building our first end-to-end Rails API!
- Here are some cool gems you can include in the
development
section of your Gemfile to make your life easier.
- pry-rails : Lets you use pry within Rails.
- hirb : Renders the output of your models as tables within the Rails console.
- rename : Allows you to easily rename your entire Rails project.