When this session is finished, you will be able to:
- Create a new Rails application with PostgreSQL
- Describe similarities between Express routing and Rails routing
- Create methods for a RESTful controller
- Create views
You should already be able to:
- Execute ruby code in
irb
orpry
- Explain the difference between HTTP request/request
- Explain MVC
- Describe SQL
Rails was created in 2003 by David Heinemeier Hansson (DHH), while working on the code base for Basecamp, a project management tool by 37signals. David extracted Ruby on Rails and officially released it as open source code in July of 2004. Despite rapid iteration of the Rails code base throughout the years, it has stuck to three basic principles:
- Ruby Programming Language
- Model-View-Controller Architecture
- Programmer Happiness
Rails was created with the goal of increasing programmers' happiness and productivity levels. In short, with Rails you can get started with a full-stack web application by quickly creating pages, templates and even query functions.
Rails heavily emphasizes "Convention over Configuration." This means that a programmer only needs to specify and code out the non-standard parts of a program. Even though Rails comes with its own set of tools and settings, you're certainly not limited to library of rails commands and configurations. Developers are free to configure their applications however they wish, though adopting conventions is certainly recommended.
For this introduction, we'll look at how to create a simple app: a cookbook! The specs for this app are as follows:
- Display a list of all recipes
- Create new recipes and edit existing recipes
- Delete recipes
To begin, we can just run the following command.
rails new cookbook -d postgresql
This command generates a new Rails app.
By default, Rails creates an application using a (rather flimsy) database called
SQLite; when using SQLite, Rails stores your application's data in a text file
inside the Rails app. This is not ideal.
However, because we added the option -d postgresql
, this app will be
configured to use a different database, Postgres, which is much more powerful
and fully featured.
We will be using Postgres for every Rails application in this course, so
get in the habit of adding that -d
option.
Let's go into the cookbook folder:
cd cookbook
Here's a list of all the files and folders that were magically created
by the rails new
command:
.
├── Gemfile
├── Gemfile.lock
├── README.md
├── Rakefile
├── app
│ ├── assets
│ │ ├── config
│ │ │ └── manifest.js
│ │ ├── images
│ │ ├── javascripts
│ │ │ ├── application.js
│ │ │ ├── cable.js
│ │ │ └── channels
│ │ └── stylesheets
│ │ └── application.css
│ ├── channels
│ │ └── application_cable
│ │ ├── channel.rb
│ │ └── connection.rb
│ ├── controllers
│ │ ├── application_controller.rb
│ │ └── concerns
│ ├── helpers
│ │ └── application_helper.rb
│ ├── jobs
│ │ └── application_job.rb
│ ├── mailers
│ │ └── application_mailer.rb
│ ├── models
│ │ ├── application_record.rb
│ │ └── concerns
│ └── views
│ └── layouts
│ ├── application.html.erb
│ ├── mailer.html.erb
│ └── mailer.text.erb
├── bin
│ ├── bundle
│ ├── rails
│ ├── rake
│ ├── setup
│ ├── spring
│ └── update
├── config
│ ├── application.rb
│ ├── boot.rb
│ ├── cable.yml
│ ├── database.yml
│ ├── environment.rb
│ ├── environments
│ │ ├── development.rb
│ │ ├── production.rb
│ │ └── test.rb
│ ├── initializers
│ │ ├── application_controller_renderer.rb
│ │ ├── assets.rb
│ │ ├── backtrace_silencers.rb
│ │ ├── cookies_serializer.rb
│ │ ├── filter_parameter_logging.rb
│ │ ├── inflections.rb
│ │ ├── mime_types.rb
│ │ ├── new_framework_defaults.rb
│ │ ├── session_store.rb
│ │ └── wrap_parameters.rb
│ ├── locales
│ │ └── en.yml
│ ├── puma.rb
│ ├── routes.rb
│ ├── secrets.yml
│ └── spring.rb
├── config.ru
├── db
│ └── seeds.rb
├── lib
│ ├── assets
│ └── tasks
├── log
├── public
│ ├── 404.html
│ ├── 422.html
│ ├── 500.html
│ ├── apple-touch-icon-precomposed.png
│ ├── apple-touch-icon.png
│ ├── favicon.ico
│ └── robots.txt
├── test
│ ├── controllers
│ ├── fixtures
│ │ └── files
│ ├── helpers
│ ├── integration
│ ├── mailers
│ ├── models
│ └── test_helper.rb
├── tmp
│ └── cache
│ └── assets
└── vendor
└── assets
├── javascripts
└── stylesheets
Wow, that's a lot of stuff. For reference, here's a list of just the top-level folders:
├── app
├── bin
├── config
├── db
├── lib
├── log
├── public
├── test
├── tmp
└── vendor
This seems really intimidating at first.
But in fact, most of these files are 'plumbing': they make up the foundation of
the Rails framework, and are unchanged from app to app.
The directories you will most regularly access and change are app
, config
,
and db
.
-
app
: 90% of the web app code will be here, including all of our model, view, and controller files. -
config
: This directory holds top-level configuration settings for your Rails app. This includes credentials for the database and other 3rd party services, settings for deployment, and instructions to the server program ('Puma' is Rails's new default) about how to serve this app over HTTP. -
db
: This directory primarily holds three things: a 'schema' file laying out the structure of the database, 'migrations', which are scripts that auto-generate the schema, and a 'seed' file, which loads whatever starter data is necessary for your app to run when in production.
Rails follow a pattern called "convention over configuration". This means that Rails expects you to follow specific patterns for how you name things and where those files 'live' inside the application. Learning conventions takes time, but they save time in the long run by helping you avoid having to make those decisions from scratch each time.
Go to the official Rails Guide and read through descriptions of the Rails file structure. Try and find answers to the following questions:
-
Where are the application's Ruby dependencies specified?
-
Where should you put third-party JavaScript and CSS files? What about files you create yourself?
-
Suppose that you needed to change your application's routes. Where would you go to do that?
We'll regroup afterwards to discuss your answers to these questions.
Rails and Express have a lot of similarities, because they were both created to solve more or less the same problem -- making it easier to quickly build web applications.
Rails uses an MVC architecture
Model - The model refers to the data objects that we use. It's the object oriented approach to design. The data in our database will be the most common type of object that we'll put there.
View - The view is the Presentation layer. It's what the user sees and interacts with, essentially the web pages. The HTML, the CSS and the JavaScript. The controller processes and responds to user events, such as clicking on links and submitting forms.
Controller - The controller will make decisions based on the request and then control what happens in response. It controls the interaction with our models and with our views.
(Ref: Hartl MVC)
Alternate Diagram:
The client is a customer eating in the restaurant, the server is the waiter, the router is waiter who hands off orders, the controller is the kitchen, the database is the giant walk-in refrigerator with ingredients, the model is the person fetching ingredients from the refrigerator, the view is the chef who makes the meal look pretty and relays it back to the customer.
Depending on how it's structured, a web application typically has three jobs:
- Respond to incoming requests.
- Manipulate underlying data.
- Serve up a response.
In Rails, the first responsibility is handled by a combination of the Router and the Controllers; Controllers hold methods that 'respond' to incoming requests, while the Router ensures that requests get passed to the right method on the right Controller.
The second responsibility is handled by Models (and, indirectly, Migrations), which provide an interface through which the Rails app can communicate with the database. A Model class represents an entire data table, while their instances each represent individual rows in the table.
Finally, that third responsibility is handled by a combination of Controllers and Views. Views are essentially template HTML files; however, they also have embedded Ruby code that can auto-generate HTML. By default, each Controller method will attempt to load and "render" a similarly-named View as soon as it finishes.
Run rails server
(or rails s
, for short) to start the server.
Open up localhost:3000
in your browser of choice.
You should get an error message. Read it -- what does the first line say?
-
database "cookbook_development" does not exist
We got this error because our app doesn't have a database yet. We can create a new database by running
rails db:create
.{ Common Pitfall }
If
rails db:create
fails, it's probably because your database server isn't running. Since we're using the Postgres app, just make sure it's on by checking for the blue elephant on your menu bar.Let's check the server again. What do we have now?
What we just did is affectionately known as EDD, or "Error-Driven Development". Because Rails has such detailed and descriptive error messages, we can usually follow a thread from error to error, with each error telling us what to do next, until our feature is fully-functional.
Some general rules for errors:
-
Don't freak out when you get an error
-
Pay attention to the first few lines -- that's usually the part of the error that tells you what the problem is.
-
Once you've found the important part of the error, don't just skim it; read it carefully, and make sure you understand what it's saying. Googling your error messages is good, but it's no substitute for actually reading the error.
Let's see this in action. We're going to use the Rails errors to build a route, a controller, and a view.
Go to
localhost:3000/recipes
to kick things off -
-
Routing Error : No route matches [GET] "/recipes"
Let's take a minute to read the entire error, together.
-
Routing Error
We have an error with our route.
-
No route matches [GET] "/recipes"
The route we asked for (GET request at "/recipes") doesn't exist.
-
Routes match in priority from top to bottom
OK, some nice general advice...
-
You don't have any routes defined!
Ah hah! Maybe that's the issue -- we need to create some routes.
-
Please add some routes in config/routes.rb
Can't get any plainer than that.
-
For more information about routes, please see the Rails guide ...
Last is a reference to a rather excellent guide on routing in Rails, "Rails Routing From The Outside In"
OK, clearly we need to add a route. Let's open up the file mentioned in the error,
config/routes.rb
, and add a new route. For reference, this file is where we define all of a Rails app's routes.Add this line inside the
do
block:get '/recipes', to: 'recipes#index'
Refresh localhost. What's the error now?
-
-
Routing Error : uninitialized constant RecipesController
This error might feel a little confusing, but that's only because we've just started learning Rails.
uninitialized constant RecipesController
means that Rails was expecting to find aRecipesController
class, but it hasn't been defined yet.By Rails convention, this class should be defined in a file called
recipes_controller.rb
inside theapp/controllers
directory.Let's fix this error.
-
Create
recipes_controller.rb
-
In that file, define a class called
RecipesController
. -
Make this new Controller class inherit from a predefined Controller class called
ApplicationController
;ApplicationController
defines the default Controller configuration for your application, and itself inherits from the baseline Controller class,ActionController
.
{ Common Pitfall } Both the filenames and the class names must be PLURAL for Controllers
Refresh localhost. We should have another error...
-
-
The action 'index' could not be found for RecipesController
RecipesController exists, but Rails is looking for an action (read: instance method) called 'index'. Why is that?
Remember what we wrote in the routes file earlier?
get '/recipes', to: 'recipes#index'
The pattern for writing routes is
http_verb 'url path', to: 'controller_name#method_name'
Therefore, what we wrote earlier means that if Rails receives a
GET
request (such as the one made by a browser when it requests a page) at the relative URL/recipes
, it should invoke theindex
method ofRecipesController
.Define an empty method in the RecipesController class called
index
Refresh the browser again.
-
ActionController::UnknownFormat in RecipesController#index
"AAAAAACHHH so much red!"
Unknown format? What? ActionController? Wat....
It's cool, read the next line.
RecipesController#index is missing a template for this request format and variant. request.formats: ["text/html"] request.variant: [] NOTE! For XHR/Ajax or API requests, this action would normally respond with 204 No Content: an empty white screen. Since you're loading it in a web browser, we assume that you expected to actually render a template, not… nothing, so we're showing an error to be extra-clear. If you expect 204 No Content, carry on. That's what you'll get from an XHR or API request. Give it a shot.
As mentioned earlier, Controller methods attempt to render View templates as soon as they finish executing. In this case, Rails wasn't able to find a file called
index.html.erb
(the template for anindex
method) at the pathviews/recipes
.Make a new
recipes
directory inside theviews
directory.{ Common Pitfall } View directories are PLURAL, just like Controllers.
Then, create a new file inside it called
index.html.erb
..html.erb
means "html with embedded ruby" -- in other words, Ruby code interspersed with HTML code. If you've worked with.ejs
in Express, the idea is the same.Let's add some HTML to this new file:
<h1>HELLOO WORLDD</h1>
Refresh the browser -- everything should be working now!
-
Now that everything's rendering, let's make a modification to the Controller and View to bring it closer to how things will actually work in Rails.
Instance variables (
@...
) in the Controller are visible to their respective views, so instead of hard-coding "HELLOO WORLDD" in the View, let's pull it out into the Controller and store it in an instance variable, which we can then access from the View.def index @example_data = "HELLOO WORLDD" end
<%= @example_data %>
So that's the core of creating routes and rendering views.
Let's review the flow of what we just did:
-
A http request comes in from the browser
-
Rails first checks the routes.rb file to see if the path and verb match anything written in there.
-
When it finds a match it directs the http request to the controller and method specified. i.e.
recipes#index
directs to theindex
method of RecipesController -
Rails then looks in the views directory for a directory name that matches the name of the controller and subsequently for a file that matches the name of the method. i.e.
recipes/index.html.erb
-
Rails creates an HTML page by rendering the embedded Ruby and variables in the View, if any, and sends that page back to the browser.
The last part of MVC is the Model, but we'll come back to that after practising routing.
-
You're going to begin creating a new Rails app that helps people log their favorite jokes. When you're finshed, you should have:
-
A Jokes controller
-
Routes for new, index and show actions on that controller. (they all follow the exact same pattern as what we just did for
recipes#index
) -
A Jokes views directory
-
Three html.erb files associated with new, index, and show
Go step by step. Even if you remember what to do exactly, use our EDD process; not only will it help you familiarize yourself with Rails errors and what causes them, but it's also just a good habit to get in!
{PROTIP} run $ rails routes to see the routes that exist in config/routes.rb
Real Rails applications use some sort of database to manage their data. The primary tools Rails uses to manage the database are Models and Migrations. We'll look at Migrations first.
Take 10 minutes to read about migrations in the Rails guides. DO NOT TRY TO MIGRATE YET. We need to set up our migrations first.
-
Set up a migration for Recipes.
Like almost every part of Rails, migrations can be generated from the command line.
rails g migration CreateRecipes
Make sure when generating migrations you use the right naming The name of the migration should be in camelcase, start with the name of what your script does, and end with the (plural) name of your resource.
BAD
rails g migration users
rails g migration AddUser
rails g migration CreateUser
GOODrails g migration CreateUsers
rails g migration AddFieldToUsers
rails g migration RemoveFieldFromUsers
We can see from the output that it has created a new directory,
migrate
, and a file for us inside of thedb
directory. This file is a migration file, and it contains a script which will be executed on the database.If we look inside the file, it's clear that there's a very specific syntax. One might say that it's an example of a Domain-Specific Language, or DSL. Rails has many such examples, as we saw in the routes file.
def change create_table :recipes do |t| t.string :title t.text :description t.boolean :made_it_yet, default: false t.timestamps end end
Fundamentally, a migration represents a change to the structure of the database -- anything that involves adding/modifying/deleting columns, or dropping or creating new tables.
Once you've created a migration file, you can execute it by running the command
rails db:migrate
in the console. To undo the last completed migration, runrails db:rollback
.Read more about migrations here
You can look at the database using
rails db
, and if you wanted, you could create new Recipes by hand by writing SQL code. But Rails, of course, has a simpler way : Models -
Inside
app/models
, create a new file calledrecipe.rb
. Inside it, define a class calledRecipe
; have it inherit fromApplicationRecord
, just like how we hadRecipesController
inherit fromApplicationController
.{Common Pitfall} Although Controllers and Views use plural names, Models use SINGULAR names.
The Recipe class represents the entire table of Recipes. You can call
.new
on the class to create new Recipe instances, each of which represents one recipe in the table.By inheriting from
ApplicationRecord
, we not only have a Ruby way of representing our SQL entities but we also have access to a set of functions for all the basic DB CRUD functions and for querying. Here are a few examples.-
.all
returns a collection of Recipe instances, one representing each entry in therecipes
table. -
.find(id)
takes an id, and returns a Recipe instance for the row in in therecipes
table for which theid
field equals the id passed in. -
.where(query)
allows you to pass in a search query, and returns a a collection of matching records.
-
Let's play around with the Model using the Rails console, a Ruby REPL
run from within Rails. To open the console, type rails c
.
Read through this section in the docs and try out some of these commands/patterns with our Recipe model. (The docs use User, just replace that with Recipe)
Go back through the steps in the code-alone in pairs, and use the instructions
to create a jokes
table in the database, and make it accessible using a Joke
model. A Joke should have:
-
a setup ("Why did the chicken cross the road?")
-
a punchline ("To get to the other side!")
-
a "dad score" indicating how lame the joke is (0 to 10, with 10 being super-lame)
Test your work in the Rails console.
In the last exercise, you created some new recipes in the database using the Recipe model. Models are all accessible from every part of the Rails application, which means that you can write Ruby scripts that use models to perform CRUD. This also means that we can access the models from controller methods, and tie incoming requests to CRUD actions on the database.
Let's trying doing this from within the index
method of our RecipesController.
We can use the .all
method to get a collection of Recipes.
If we then take that collection and put it in an instance variable, it will
become accessible from the View!
def index
@recipes = Recipe.all
end
In recipes/index.html.erb
, we can add <%= @recipes %>
to the page.
If we go to localhost:3000/recipes
in the browser, we'll see... something.
According to the Rails console, we're retrieving a list of all recipes. But it's not showing up in a meaningful format.
Fortunately, because it's an .erb
file, we can write Ruby code to loop through
the collection of recipes and show some snippet of HTML for each one.
<% @recipes.each do |recipe| %>
<ul>
<lh><%= recipe.title %></lh>
<li><%= recipe.description %></li>
<li><%= recipe.made_it_yet %></li>
</ul>
<% end %>
And voila! Our recipes are visible on the page.
Modify the controller and views in your Joke app so that /jokes
shows a list
of all jokes. You can lay them our as a table or as items in a list, but please
make all the properties human-readable on the page.
Now that we've taken data from the database and shown it on a view, let's go the opposite way, and use HTML forms to take data from the view back to the model and the database.
Take a quick skim through the form helpers documentation and try to answer the following questions:
- What's the difference between
form_tag
andform_for
? - How do you create a text field within a form?
- How do you create a 'submit' button for a form?
We're going to use the form_for
helper to build a form for creating new
Recipes, and then wire it up to the controller and model so that we can actually
alter the database.
-
Add routes for two new actions,
new
andcreate
. The standard convention would be forrecipes#new
to be triggered by aGET
request at/recipes/new
, and forrecipes#create
to be triggered by aPOST
request at/recipes
. -
Make two new methods on
RecipesController
:new
andcreate
. -
Create a view for the
new
method inside/views/recipes/new.index.html
. This view should use theform_for
helper to bind the form to the instance variable@recipe
, which we will soon make available from the controller.<%= form_for @recipe do |f| %> <div> <%= f.label :title %> <%= f.text_field :title %> </div> <div> <%= f.label :description %> <%= f.text_area :description %> </div> <div> <%= f.label :made_it_yet %> <%= f.check_box :made_it_yet %> </div> <%= f.submit "New Recipe" %> <% end %>
By default, submitting the form will trigger the associated
create
action. But first, let's take care of thenew
action. -
The view we just created will render after the
new
method runs. It is looking for an instance variable@recipe
, which will hold a blank recipe. We'll need to provide this in the controller method.def new @recipe = Recipe.new end
With this done, the form should load in the browser.
-
Lastly, let's handle the
create
method. Ever controller has a special method calledparams
which returns, as a hash, a bunch of keys and values related to the current request.def create puts params end
When the form we just made gets submitted,
RecipesController
will receive aPOST
request; when it does, this hash will include, along with all normal properties, a property calledrecipe
, withtitle
,description
, etc as sub-properties.# This is what the 'params hash' looks like. { ... :recipe => { :title => 'some title', :description => 'some description', :made_it_yet => false } }
We want to keep only the parts of the hash pertaining to the recipe. Let's define a private method called
recipe_params
to do that. Inside, we can use two helper methods,require
andpermit
, to define (respectively) the root key and the sub-keys that we want to keep.private def recipe_params params.require(:recipe).permit(:title, :description, :made_it_yet) end
Why limit the sub-keys? It's possible that soemone might maliciously try to muck with our app by changing properties that we may not want them changing (timestamps, for instance). This approach is called using "strong params", and it's a very simple security measure that every Rails app should use.
Now let's use these "curated" parameters to create a new recipe.
def create @recipe = Recipe.new(recipe_params) if @recipe.save redirect_to recipes_url else redirect_to new_recipe_url end end
This highlights a very important point. Although the model represents the data in the table, it isn't the same thing. Modifying a model instance does not automatically modify the underlying table. Instead, you'll need to invoke the
save
method for them to sync.If we can save this recipe successfully, our app will redirect the user to
recipes_url
(a shortcut for/recipes
); if it does not save successfully, it will send them tonew_recipe_url
(/recipes/new
), i.e. the original form.{PROTIP} You can see a list of all routes (including request type, URL, controller name, action name, and prefixes for the shortcut names) by running
rails routes
.When we complete the form, we can see our new recipe show up on the list!
As we did in the code along section, create new
and create
actions for
JokesController
, set up the appropriate routes, and add a form to your
new.html.erb
view. Finally, link up the form to the controller, add a
"strong params" method, and wire up create
so that you can make new jokes by
filling out the form.