123456789_123456789_123456789_123456789_123456789_

DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.

Getting Started with Rails

This guide covers getting up and running with Ruby on Rails.

After reading this guide, you will know:


Introduction

Welcome to Ruby on Rails! In this guide, we'll walk through the core concepts of building web applications with Rails. You don't need any experience with Rails to follow along with this guide.

Rails is a web framework built for the Ruby programming language. Rails takes advantage of many features of Ruby so we strongly recommend learning the basics of Ruby so that you understand some of the basic terms and vocabulary you will see in this tutorial.

Rails Philosophy

Rails is a web application development framework written in the Ruby programming language. It is designed to make programming web applications easier by making assumptions about what every developer needs to get started. It allows you to write less code while accomplishing more than many other languages and frameworks. Experienced Rails developers also report that it makes web application development more fun.

Rails is opinionated software. It makes the assumption that there is a "best" way to do things, and it's designed to encourage that way - and in some cases to discourage alternatives. If you learn "The Rails Way" you'll probably discover a tremendous increase in productivity. If you persist in bringing old habits from other languages to your Rails development, and trying to use patterns you learned elsewhere, you may have a less happy experience.

The Rails philosophy includes two major guiding principles:

Creating a New Rails App

We're going to build a project called store - a simple e-commerce app that demonstrates several of Rails' built-in features.

TIP: Any commands prefaced with a dollar sign $ should be run in the terminal.

Prerequisites

For this project, you will need:

Follow the Install Ruby on Rails Guide if you need to install Ruby and/or Rails.

Let's verify the correct version of Rails is installed. To display the current version, open a terminal and run the following. You should see a version number printed out:

$ rails --version
Rails 8.2.0

The version shown should be Rails 8.2.0 or higher.

Creating Your First Rails App

Rails comes with several commands to make life easier. Run rails --help to see all of the commands.

rails new generates the foundation of a fresh Rails application for you, so let's start there.

To create our store application, run the following command in your terminal:

$ rails new store

NOTE: You can customize the application Rails generates by using flags. To see these options, run rails new --help.

After your new application is created, switch to its directory:

$ cd store

Directory Structure

Let's take a quick glance at the files and directories that are included in a new Rails application. You can open this folder in your code editor or run ls -la in your terminal to see the files and directories.

File/Folder Purpose
app/ Contains the controllers, models, views, helpers, mailers, jobs, and assets for your application. You'll focus mostly on this folder for the remainder of this guide.
bin/ Contains the rails script that starts your app and can contain other scripts you use to set up, update, deploy, or run your application.
config/ Contains configuration for your application's routes, database, and more. This is covered in more detail in Configuring Rails Applications.
config.ru Rack configuration for Rack-based servers used to start the application.
db/ Contains your current database schema, as well as the database migrations.
Dockerfile Configuration file for Docker.
Gemfile
Gemfile.lock
These files allow you to specify what gem dependencies are needed for your Rails application. These files are used by the Bundler gem.
lib/ Extended modules for your application.
log/ Application log files.
public/ Contains static files and compiled assets. When your app is running, this directory will be exposed as-is.
Rakefile This file locates and loads tasks that can be run from the command line. The task definitions are defined throughout the components of Rails. Rather than changing Rakefile, you should add your own tasks by adding files to the lib/tasks directory of your application.
README.md This is a brief instruction manual for your application. You should edit this file to tell others what your application does, how to set it up, and so on.
script/ Contains one-off or general purpose scripts](https://github.com/rails/rails/blob/main/railties/lib/rails/generators/rails/script/USAGE) and [benchmarks.
storage/ Contains SQLite databases and Active Storage files for Disk Service. This is covered in Active Storage Overview.
test/ Unit tests, fixtures, and other test apparatus. These are covered in Testing Rails Applications.
tmp/ Temporary files (like cache and pid files).
vendor/ A place for all third-party code. In a typical Rails application this includes vendored gems.
.dockerignore This file tells Docker which files it should not copy into the container.
.gitattributes This file defines metadata for specific paths in a Git repository. This metadata can be used by Git and other tools to enhance their behavior. See the gitattributes documentation for more information.
.git/ Contains Git repository files.
.github/ Contains GitHub specific files.
.gitignore This file tells Git which files (or patterns) it should ignore. See GitHub - Ignoring files for more information about ignoring files.
.kamal/ Contains Kamal secrets and deployment hooks.
.rubocop.yml This file contains the configuration for RuboCop.
.ruby-version This file contains the default Ruby version.

Model-View-Controller Basics

Rails code is organized using the Model-View-Controller (MVC) architecture. With MVC, we have three main concepts where the majority of our code lives:

Now that we've got a basic understanding of MVC, let's see how it's used in Rails.

Hello, Rails!

Let's start easy by creating our application's database and boot up our Rails server for the first time.

In your terminal, run the following commands in the store directory:

$ bin/rails db:create

This will initially create the application's database.

$ bin/rails server

NOTE: When we run commands inside an application directory, we should use bin/rails. This makes sure the application's version of Rails is used.

This will start up a web server called Puma that will serve static files and your Rails application:

=> Booting Puma
=> Rails 8.2.0 application starting in development
=> Run <code>bin/rails server --help</code> for more startup options
Puma starting in single mode...
* Puma version: 6.4.3 (ruby 3.3.5-p100) ("The Eagle of Durango")
*  Min threads: 3
*  Max threads: 3
*  Environment: development
*          PID: 12345
* Listening on http://127.0.0.1:3000
* Listening on http://[::1]:3000
Use Ctrl-C to stop

To see your Rails application, open http://localhost:3000 in your browser. You will see the default Rails welcome page:

!Rails welcome page

It works!

This page is the smoke test for a new Rails application, ensuring that everything is working behind the scenes to serve a page.

To stop the Rails server anytime, press Ctrl-C in your terminal.

Autoloading in Development

Developer happiness is a cornerstone philosophy of Rails and one way of achieving this is with automatic code reloading in development.

Once you start the Rails server, new files or changes to existing files are detected and automatically loaded or reloaded as necessary. This allows you to focus on building without having to restart your Rails server after every change.

You may also notice that Rails applications rarely use require statements like you may have seen in other programming languages. Rails uses naming conventions to require files automatically so you can focus on writing your application code.

See Autoloading and Reloading Constants for more details.

Creating a Database Model

Active Record is a feature of Rails that maps relational databases to Ruby code. It helps generate the structured query language (SQL) for interacting with the database like creating, updating, and deleting tables and records. Our application is using SQLite which is the default for Rails.

Let's start by adding a database table to our Rails application to add products to our simple e-commerce store.

$ bin/rails generate model Product name:string

This command tells Rails to generate a model named Product which has a name column and type of string in the database. Later on, you'll learn how to add other column types.

You'll see the following in your terminal:

      invoke  active_record
      create    db/migrate/20240426151900_create_products.rb
      create    app/models/product.rb
      invoke    test_unit
      create      test/models/product_test.rb
      create      test/fixtures/products.yml

This command does several things. It creates...

  1. A migration in the db/migrate folder.
  2. An Active Record model in app/models/product.rb.
  3. Tests and test fixtures for this model.

NOTE: Model names are singular, because an instantiated model represents a single record in the database (i.e., You are creating a product to add to the database.).

Database Migrations

A migration is a set of changes we want to make to our database.

By defining migrations, we're telling Rails how to change the database to add, change, or remove tables, columns or other attributes of our database. This helps keep track of changes we make in development (only on our computer) so they can be deployed to production (live, online!) safely.

In your code editor, open the migration Rails created for us so we can see what the migration does.

NOTE: As convention we'll state the filepath as a comment on top of each file

# db/migrate/<timestamp>_create_products.rb
class CreateProducts < ActiveRecord::Migration[8.2]
  def change
    create_table :products do |t|
      t.string :name

      t.timestamps
    end
  end
end

This migration is telling Rails to create a new database table named products.

NOTE: In contrast to the model above, Rails makes the database table names plural, because the database holds all of the instances of each model (i.e., You are creating a database of products).

The create_table block then defines which columns and types should be defined in this database table.

t.string :name tells Rails to create a column in the products table called name and set the type as string.

t.timestamps is a shortcut for defining two columns on your models: created_at:datetime and updated_at:datetime. You'll see these columns on most Active Record models in Rails and they are automatically set by Active Record when creating or updating records.

Running Migrations

Now that you have defined what changes to make to the database, use the following command to run the migrations:

$ bin/rails db:migrate

This command checks for any new migrations and applies them to your database. Its output looks like this:

== 20240426151900 CreateProducts: migrating ===================================
-- create_table(:products)
   -> 0.0030s
== 20240426151900 CreateProducts: migrated (0.0031s) ==========================

TIP: If you make a mistake, you can run bin/rails db:rollback to undo the last migration.

Rails Console

Now that we have created our products table, we can interact with it in Rails. Let's try it out.

For this, we're going to use a Rails feature called the console. The console is a helpful, interactive tool for testing our code in our Rails application.

$ bin/rails console

You will be presented with a prompt like the following:

Loading development environment (Rails 8.2.0)
store(dev)>

Here we can type code that will be executed when we hit Enter. Let's try printing out the Rails version:

store(dev)> {Rails.version}
=> "8.2.0"

It works!

Active Record Model Basics

When we ran the Rails model generator to create the Product model, it created a file at app/models/product.rb. This file creates a class that uses Active Record for interacting with our products database table.

# app/models/product.rb
class Product < ApplicationRecord
end

You might be surprised that there is no code in this class. How does Rails know what defines this model?

When the Product model is used, Rails will query the database table for the column names and types and automatically generate code for these attributes. Rails saves us from writing this boilerplate code and instead takes care of it for us behind the scenes so we can focus on our application logic instead.

Let's use the Rails console to see what columns Rails detects for the Product model.

Run:

store(dev)> {Product.column_names}

And you should see:

=> ["id", "name", "created_at", "updated_at"]

Rails asked the database for column information above and used that information to define attributes on the Product class dynamically so you don't have to manually define each of them. This is one example of how Rails makes development a breeze.

Creating Records

We can instantiate a new Product record with the following code:

store(dev)> product = {Product.new}(name: "T-Shirt")
=> {#<}Product:0x000000012e616c30 id: nil, name: "T-Shirt", created_at: nil, updated_at: nil>

The product variable is an instance of Product. It has not been saved to the database, and so does not have an ID, created_at, or updated_at timestamps.

We can call save to write the record to the database.

store(dev)> product.save
  TRANSACTION (0.1ms)  BEGIN immediate TRANSACTION /*application='Store'*/
  Product Create (0.9ms)  INSERT INTO "products" ("name", "created_at", "updated_at") VALUES ('T-Shirt', '2024-11-09 16:35:01.117836', '2024-11-09 16:35:01.117836') RETURNING "id" /*application='Store'*/
  TRANSACTION (0.9ms)  COMMIT TRANSACTION /*application='Store'*/
=> true

When save is called, Rails takes the attributes in memory and generates an INSERT SQL query to insert this record into the database.

Rails also updates the object in memory with the database record id along with the created_at and updated_at timestamps. We can see that by printing out the product variable.

store(dev)> product
=> {#<}Product:0x00000001221f6260 id: 1, name: "T-Shirt", created_at: "2024-11-09 16:35:01.117836000 +0000", updated_at: "2024-11-09 16:35:01.117836000 +0000">

Similar to save, we can use create to instantiate and save an Active Record object in a single call.

store(dev)> {Product.create}(name: "Pants")
  TRANSACTION (0.1ms)  BEGIN immediate TRANSACTION /*application='Store'*/
  Product Create (0.4ms)  INSERT INTO "products" ("name", "created_at", "updated_at") VALUES ('Pants', '2024-11-09 16:36:01.856751', '2024-11-09 16:36:01.856751') RETURNING "id" /*application='Store'*/
  TRANSACTION (0.1ms)  COMMIT TRANSACTION /*application='Store'*/
=> {#<}Product:0x0000000120485c80 id: 2, name: "Pants", created_at: "2024-11-09 16:36:01.856751000 +0000", updated_at: "2024-11-09 16:36:01.856751000 +0000">

Querying Records

We can also look up records from the database using our Active Record model.

To find all the Product records in the database, we can use the all method. This is a class method, which is why we can use it on Product (versus an instance method that we would call on the product instance, like save above).

store(dev)> {Product.all}
  Product Load (0.1ms)  SELECT "products".* FROM "products" /* loading for pp */ LIMIT 11 /*application='Store'*/
=> [#<Product:0x0000000121845158 id: 1, name: "T-Shirt", created_at: "2024-11-09 16:35:01.117836000 +0000", updated_at: "2024-11-09 16:35:01.117836000 +0000">,
 #<Product:0x0000000121845018 id: 2, name: "Pants", created_at: "2024-11-09 16:36:01.856751000 +0000", updated_at: "2024-11-09 16:36:01.856751000 +0000">]

This generates a SELECT SQL query to load all records from the products table. Each record is automatically converted into an instance of our Product Active Record model so we can easily work with them from Ruby.

TIP: The all method returns an ::ActiveRecord::Relation object which is an Array-like collection of database records with features to filter, sort, and execute other database operations.

Filtering & Ordering Records

What if we want to filter the results from our database? We can use where to filter records by a column.

store(dev)> {Product.where}(name: "Pants")
  Product Load (1.5ms)  SELECT "products".* FROM "products" WHERE "products"."name" = 'Pants' /* loading for pp */ LIMIT 11 /*application='Store'*/
=> [#<Product:0x000000012184d858 id: 2, name: "Pants", created_at: "2024-11-09 16:36:01.856751000 +0000", updated_at: "2024-11-09 16:36:01.856751000 +0000">]

This generates a SELECT SQL query but also adds a WHERE clause to filter the records that have a name matching "Pants". This also returns an ::ActiveRecord::Relation because multiple records may have the same name.

We can use order(name: :asc) to sort records by name in ascending alphabetical order.

store(dev)> {Product.order}(name: :asc)
  Product Load (0.3ms)  SELECT "products".* FROM "products" /* loading for pp */ ORDER BY "products"."name" ASC LIMIT 11 /*application='Store'*/
=> [#<Product:0x0000000120e02a88 id: 2, name: "Pants", created_at: "2024-11-09 16:36:01.856751000 +0000", updated_at: "2024-11-09 16:36:01.856751000 +0000">,
 #<Product:0x0000000120e02948 id: 1, name: "T-Shirt", created_at: "2024-11-09 16:35:01.117836000 +0000", updated_at: "2024-11-09 16:35:01.117836000 +0000">]

Finding Records

What if we want to find one specific record?

We can do this by using the find class method to look up a single record by ID. Call the method and pass in the specific ID by using the following code:

store(dev)> {Product.find}(1)
  Product Load (0.2ms)  SELECT "products".* FROM "products" WHERE "products"."id" = 1 LIMIT 1 /*application='Store'*/
=> {#<}Product:0x000000012054af08 id: 1, name: "T-Shirt", created_at: "2024-11-09 16:35:01.117836000 +0000", updated_at: "2024-11-09 16:35:01.117836000 +0000">

This generates a SELECT query but specifies a WHERE for the id column matching the ID of 1 that was passed in. It also adds a LIMIT to only return a single record.

This time, we get a Product instance instead of an ::ActiveRecord::Relation since we're only retrieving a single record from the database.

Updating Records

Records can be updated in 2 ways: using update or assigning attributes and calling save.

We can call update on a Product instance and pass in a Hash of new attributes to save to the database. This will assign the attributes, run validations, and save the changes to the database in one method call.

store(dev)> product = {Product.find}(1)
store(dev)> product.update(name: "Shoes")
  TRANSACTION (0.1ms)  BEGIN immediate TRANSACTION /*application='Store'*/
  Product Update (0.3ms)  UPDATE "products" SET "name" = 'Shoes', "updated_at" = '2024-11-09 22:38:19.638912' WHERE "products"."id" = 1 /*application='Store'*/
  TRANSACTION (0.4ms)  COMMIT TRANSACTION /*application='Store'*/
=> true

This updated the name of the "T-Shirt" product to "Shoes" in the database. Confirm this by running Product.all again.

store(dev)> {Product.all}

You will see two products: Shoes and Pants.

  Product Load (0.3ms)  SELECT "products".* FROM "products" /* loading for pp */ LIMIT 11 /*application='Store'*/
=>
[#<Product:0x000000012c0f7300
  id: 1,
  name: "Shoes",
  created_at: "2024-12-02 20:29:56.303546000 +0000",
  updated_at: "2024-12-02 20:30:14.127456000 +0000">,
 #<Product:0x000000012c0f71c0
  id: 2,
  name: "Pants",
  created_at: "2024-12-02 20:30:02.997261000 +0000",
  updated_at: "2024-12-02 20:30:02.997261000 +0000">]

Alternatively, we can assign attributes and call save when we're ready to validate and save changes to the database.

Let's change the name "Shoes" back to "T-Shirt".

store(dev)> product = {Product.find}(1)
store(dev)> product.name = "T-Shirt"
=> "T-Shirt"
store(dev)> product.save
  TRANSACTION (0.1ms)  BEGIN immediate TRANSACTION /*application='Store'*/
  Product Update (0.2ms)  UPDATE "products" SET "name" = 'T-Shirt', "updated_at" = '2024-11-09 22:39:09.693548' WHERE "products"."id" = 1 /*application='Store'*/
  TRANSACTION (0.0ms)  COMMIT TRANSACTION /*application='Store'*/
=> true

Deleting Records

The destroy method can be used to delete a record from the database.

store(dev)> product.destroy
  TRANSACTION (0.1ms)  BEGIN immediate TRANSACTION /*application='Store'*/
  Product Destroy (0.4ms)  DELETE FROM "products" WHERE "products"."id" = 1 /*application='Store'*/
  TRANSACTION (0.1ms)  COMMIT TRANSACTION /*application='Store'*/
=> {#<}Product:0x0000000125813d48 id: 1, name: "T-Shirt", created_at: "2024-11-09 22:39:38.498730000 +0000", updated_at: "2024-11-09 22:39:38.498730000 +0000">

This deleted the T-Shirt product from our database. We can confirm this with Product.all to see that it only returns Pants.

store(dev)> {Product.all}
  Product Load (1.9ms)  SELECT "products".* FROM "products" /* loading for pp */ LIMIT 11 /*application='Store'*/
=>
[#<Product:0x000000012abde4c8
  id: 2,
  name: "Pants",
  created_at: "2024-11-09 22:33:19.638912000 +0000",
  updated_at: "2024-11-09 22:33:19.638912000 +0000">]

Validations

Active Record provides validations which allows you to ensure data inserted into the database adheres to certain rules.

Let's add a presence validation to the Product model to ensure that all products must have a name.

# app/models/product.rb
class Product < ApplicationRecord
  validates :name, presence: true
end

You might remember that Rails automatically reloads changes during development. However, if the console is running when you make updates to the code, you'll need to manually refresh it. So let's do this now by running 'reload!'.

store(dev)> reload!
Reloading...

Let's try to create a Product without a name in the Rails console.

store(dev)> product = {Product.new}
store(dev)> product.save
=> false

This time save returns false because the name attribute wasn't specified.

Rails automatically runs validations during create, update, and save operations to ensure valid input. To see a list of errors generated by validations, we can call errors on the instance.

store(dev)> product.errors
=> {#<}ActiveModel::Errors [#<ActiveModel::Error attribute=name, type=blank, options={}>]>

This returns an ::ActiveModel::Errors object that can tell us exactly which errors are present.

It also can generate friendly error messages for us that we can use in our user interface.

store(dev)> product.errors.full_messages
=> ["Name can't be blank"]

Now let's build a web interface for our Products.

We are done with the console for now, so you can exit out of it by running exit.

A Request's Journey Through Rails

To get Rails saying "Hello", you need to create at minimum a route, a controller with an action, and a view. A route maps a request to a controller action. A controller action performs the necessary work to handle the request, and prepares any data for the view. A view displays data in a desired format.

In terms of implementation: Routes are rules written in a Ruby DSL (Domain-Specific Language). Controllers are Ruby classes, and their public methods are actions. And views are templates, usually written in a mixture of HTML and Ruby.

That's the short of it, but we’re going to walk through each of these steps in more detail next.

Routes

In Rails, a route is the part of the URL that determines how an incoming HTTP request is directed to the appropriate controller and action for processing. First, let's do a quick refresher on URLs and HTTP Request methods.

Parts of a URL

Let's examine the different parts of a URL:

https://example.org/products?sale=true&sort=asc

In this URL, each part has a name:

HTTP Methods and Their Purpose

HTTP requests use methods to tell a server what action it should perform for a given URL. Here are the most common methods:

Rails Routes

A route in Rails refers to a line of code that pairs an HTTP Method and a URL path. The route also tells Rails which controller and action should respond to a request.

To define a route in Rails, let's go back to your code editor and add the following route to config/routes.rb

# config/routes.rb
Rails.application.routes.draw do
  # ...
  get "/products", to: "products#index"
end

This route tells Rails to look for GET requests to the /products path. In this example, we specified "products#index" for where to route the request.

When Rails sees a request that matches, it will send the request to the ProductsController and the index action inside of that controller. This is how Rails will process the request and return a response to the browser.

You'll notice that we don't need to specify the protocol, domain, or query params in our routes. That's basically because the protocol and domain make sure the request reaches your server. From there, Rails picks up the request and knows which path to use for responding to the request based on what routes are defined. The query params are like options that Rails can use to apply to the request, so they are typically used in the controller for filtering the data.

Let's look at another example. Add this line after the previous route:

# config/routes.rb
Rails.application.routes.draw do
  # ...
  get "/products", to: "products#index"
  post "/products", to: "products#create"
end

Here, we've told Rails to take POST requests to "/products" and process them with the ProductsController using the create action.

Routes may also need to match URLs with certain patterns. So how does that work?

# config/routes.rb
Rails.application.routes.draw do
  # ...
  get "/products/:id", to: "products#show"
end

This route has :id in it. This is called a parameter and it captures a portion of the URL to be used later for processing the request.

If a user visits /products/1, the :id param is set to 1 and can be used in the controller action to look up and display the Product record with an ID of 1. /products/2 would display Product with an ID of 2 and so on.

Route parameters don't have to be Integers, either.

For example, you could have a blog with articles and match /blog/hello-world with the following route:

# config/routes.rb
Rails.application.routes.draw do
  # ...
  get "/blog/:title", to: "blog#show"
end

Rails will capture hello-world out of /blog/hello-world and this can be used to look up the blog post with the matching title.

CRUD Routes

There are 4 common actions you will generally need for a resource: Create, Read, Update, Delete (CRUD). This translates to 8 typical routes:

We can add routes for these CRUD actions with the following:

# config/routes.rb
Rails.application.routes.draw do
  # ...
  get "/products", to: "products#index"

  get "/products/new", to: "products#new"
  post "/products", to: "products#create"

  get "/products/:id", to: "products#show"

  get "/products/:id/edit", to: "products#edit"
  patch "/products/:id", to: "products#update"
  put "/products/:id", to: "products#update"

  delete "/products/:id", to: "products#destroy"
end

Resource Routes

Typing out these routes every time is redundant, so Rails provides a shortcut for defining them. To create all of the same CRUD routes, replace the above routes with this single line:

# config/routes.rb
Rails.application.routes.draw do
  # ...
  resources :products
end

TIP: If you don’t want all these CRUD actions, you specify exactly what you need. Check out the routing guide for details.

Routes Command

Rails provides a command that displays all the routes your application responds to.

In your terminal, run the following command.

$ bin/rails routes

You'll see this in the output which are the routes generated by resources :products

      Prefix Verb   URI Pattern                  Controller#Action
    products GET    /products(.:format)          products#index
             POST   /products(.:format)          products#create
 new_product GET    /products/new(.:format)      products#new
edit_product GET    /products/:id/edit(.:format) products#edit
     product GET    /products/:id(.:format)      products#show
             PATCH  /products/:id(.:format)      products#update
             PUT    /products/:id(.:format)      products#update
             DELETE /products/:id(.:format)      products#destroy

You'll also see routes from other built-in Rails features like health checks.

Controllers & Actions

Now that we've defined routes for Products, let's implement the controller and actions to handle requests to these URLs.

This command will generate a ProductsController with an index action. Since we've already set up routes, we can skip that part of the generator using a flag.

$ bin/rails generate controller Products index --skip-routes
      create  app/controllers/products_controller.rb
      invoke  erb
      create    app/views/products
      create    app/views/products/index.html.erb
      invoke  test_unit
      create    test/controllers/products_controller_test.rb
      invoke  helper
      create    app/helpers/products_helper.rb
      invoke    test_unit

This command generates a handful of files for our controller:

Let's take a look at the ProductsController defined in app/controllers/products_controller.rb. It looks like this:

# app/controllers/products_controller.rb
class ProductsController < ApplicationController
  def index
  end
end

NOTE: You may notice the file name products_controller.rb is an underscored version of the Class this file defines, ProductsController. This pattern helps Rails to automatically load code without having to use require like you may have seen in other languages.

The index method here is an Action. Even though it's an empty method, Rails will default to rendering a template with the matching name.

The index action will render app/views/products/index.html.erb. If we open up that file in our code editor, we'll see the HTML it renders.

<%# app/views/products/index.html.erb %>
<h1>Products#index</h1>
<p>Find me in app/views/products/index.html.erb</p>

Making Requests

Let's see this in our browser. First, run bin/rails server in your terminal to start the Rails server. Then open http://localhost:3000 and you will see the Rails welcome page.

If we open http://localhost:3000/products in the browser, Rails will render the products index HTML.

Our browser requested /products and Rails matched this route to products#index. Rails sent the request to the ProductsController and called the index action. Since this action was empty, Rails rendered the matching template at app/views/products/index.html.erb and returned that to our browser. Pretty cool!

If we open config/routes.rb, we can tell Rails the root route should render the Products index action by adding this line:

# config/routes.rb
Rails.application.routes.draw do
  # ...
  root "products#index"
  resources :products
end

Now when you visit http://localhost:3000, Rails will render Products#index.

Instance Variables

Let's take this a step further and render some records from our database.

In the index action, let's add a database query and assign it to an instance variable. Rails uses instance variables (variables that start with an @) to share data with the views.

# app/controllers/products_controller.rb
class ProductsController < ApplicationController
  def index
    @products = Product.all
  end
end

In app/views/products/index.html.erb, we can replace the HTML with this ERB:

<%# app/views/products/index.html.erb %>
<%= debug @products %>

ERB is short for Embedded Ruby and allows us to execute Ruby code to dynamically generate HTML with Rails. The <%= %> tag tells ERB to execute the Ruby code inside and output the return value. In our case, this takes @products, converts it to YAML, and outputs the YAML.

Now refresh http://localhost:3000/ in your browser and you'll see that the output has changed. What you're seeing is the records in your database being displayed in YAML format.

The debug helper prints out variables in YAML format to help with debugging. For example, if you weren't paying attention and typed singular @product instead of plural @products, the debug helper could help you identify that the variable was not set correctly in the controller.

TIP: Check out the Action View Helpers guide to see more helpers that are available.

Let's update app/views/products/index.html.erb to render all of our product names.

<%# app/views/products/index.html.erb %>
<h1>Products</h1>

<div id="products">
  <% @products.each do |product| %>
    <div>
      <%= product.name %>
    </div>
  <% end %>
</div>

Using ERB, this code loops through each product in the @products ::ActiveRecord::Relation object and renders a

tag containing the product name.

We've used a new ERB tag this time as well. <% %> evaluates the Ruby code but does not output the return value. That ignores the output of @products.each which would output an array that we don't want in our HTML.

CRUD Actions

We need to be able to access individual products. This is the R in CRUD to read a resource.

We've already defined the route for individual products with our resources :products route. This generates /products/:id as a route that points to products#show.

Now we need to add that action to the ProductsController and define what happens when it is called.

Showing Individual Products

Open the Products controller and add the show action like this:

# app/controllers/products_controller.rb
class ProductsController < ApplicationController
  def index
    @products = Product.all
  end

  def show
    @product = Product.find(params[:id])
  end
end

The show action here defines the singular @product because it's loading a single record from the database, in other words: Show this one product. We use plural @products in index because we're loading multiple products.

To query the database, we use params to access the request parameters. In this case, we're using the :id from our route /products/:id. When we visit /products/1, the params hash contains {id: 1} which results in our show action calling Product.find(1) to load Product with ID of 1 from the database.

We need a view for the show action next. Following the Rails naming conventions, the ProductsController expects views in app/views in a subfolder named products.

The show action expects a file in app/views/products/show.html.erb. Let's create that file in our editor and add the following contents:

<%# app/views/products/show.html.erb %>
<h1><%= @product.name %></h1>

<%= link_to "Back", products_path %>

It would be helpful for the index page to link to the show page for each product so we can click on them to navigate. We can update the app/views/products/index.html.erb view to link to this new page to use an anchor tag to the path for the show action.

<%# app/views/products/index.html.erb %>
<h1>Products</h1>

<div id="products">
  <% @products.each do |product| %>
    <div>
      <a href="/products/<%= product.id %>">
        <%= product.name %>
      </a>
    </div>
  <% end %>
</div>

Refresh this page in your browser and you'll see that this works, but we can do better.

Rails provides helper methods for generating paths and URLs. When you run bin/rails routes, you'll see the Prefix column. This prefix matches the helpers you can use for generating URLs with Ruby code.

  Prefix Verb   URI Pattern             Controller#Action
products GET    /products(.:format)     products#index
 product GET    /products/:id(.:format) products#show

These route prefixes give us helpers like the following:

{_path} returns a relative path which the browser understands is for the current domain.

{_url} returns a full URL including the protocol, host, and port.

URL helpers are useful for rendering emails that will be viewed outside of the browser.

Combined with the {link_to} helper, we can generate anchor tags and use the URL helper to do this cleanly in Ruby. {link_to} accepts the display content for the link (product.name) and the path or URL to link to for the {href} attribute ({product}).

Let's refactor this to use these helpers:

<%# app/views/products/index.html.erb %>
<h1>Products</h1>

<div id="products">
  <% @products.each do |product| %>
    <div>
      <%= link_to product.name, product_path(product.id) %>
    </div>
  <% end %>
</div>

Creating Products

So far we've had to create products in the Rails console, but let's make this work in the browser.

We need to create two actions for create:

  1. The new product form to collect product information
  2. The create action in the controller to save the product and check for errors

Let's start with our controller actions.

# app/controllers/products_controller.rb
class ProductsController < ApplicationController
  def index
    @products = Product.all
  end

  def show
    @product = Product.find(params[:id])
  end

  def new
    @product = Product.new
  end
end

The {new} action instantiates a new {Product} which we will use for displaying the form fields.

We can update app/views/products/index.html.erb to link to the new action.

<%# app/views/products/index.html.erb %>
<h1>Products</h1>

<%= link_to "New product", new_product_path %>

<div id="products">
  <% @products.each do |product| %>
    <div>
      <%= link_to product.name, product_path(product.id) %>
    </div>
  <% end %>
</div>

Let's create app/views/products/new.html.erb to render the form for this new {Product}.

<%# app/views/products/new.html.erb %>
<h1>New product</h1>

<%= form_with model: @product do |form| %>
  <div>
    <%= form.label :name %>
    <%= form.text_field :name %>
  </div>

  <div>
    <%= form.submit %>
  </div>
<% end %>

<%= link_to "Cancel", products_path %>

In this view, we are using the Rails {form_with} helper to generate an HTML form to create products. This helper uses a form builder to handle things like CSRF tokens, generating the URL based upon the model: provided, and even tailoring the submit button text to the model.

If you open this page in your browser and View Source, the HTML for the form will look like this:

<form action="/products" accept-charset="UTF-8" method="post">
  <input type="hidden" name="authenticity_token" value="UHQSKXCaFqy_aoK760zpSMUPy6TMnsLNgbPMABwN1zpW-Jx6k-2mISiF0ulZOINmfxPdg5xMyZqdxSW1UK-H-Q">

  <div>
    <label for="product_name">Name</label>
    <input type="text" name="product[name]" id="product_name">
  </div>

  <div>
    <input type="submit" name="commit" value="Create Product" data-disable-with="Create Product">
  </div>
</form>

The form builder has included a CSRF token for security, configured the form for UTF-8 support, set the input field names and even added a disabled state for the submit button.

Because we passed a new {Product} instance to the form builder, it automatically generated a form configured to send a {POST} request to /products, which is the default route for creating a new record.

To handle this, we first need to implement the {create} action in our controller.

# app/controllers/products_controller.rb
class ProductsController < ApplicationController
  def index
    @products = Product.all
  end

  def show
    @product = Product.find(params[:id])
  end

  def new
    @product = Product.new
  end

  def create
    @product = Product.new(product_params)
    if @product.save
      redirect_to @product
    else
      render :new, status: :unprocessable_entity
    end
  end

  private
    def product_params
      params.expect(product: [ :name ])
    end
end

Strong Parameters

The {create} action handles the data submitted by the form, but it needs to be filtered for security. That's where the {product_params} method comes into play.

In {product_params}, we tell Rails to inspect the params and ensure there is a key named :product with an array of parameters as the value. The only permitted parameters for products are :name and Rails will ignore any other parameters. This protects our application from malicious users who might try to hack our application.

Handling Errors

After assigning these params to the new {Product}, we can try to save it to the database. @product.save tells Active Record to run validations and save the record to the database.

If {save} is successful, we want to redirect to the new product. When {redirect_to} is given an Active Record object, Rails generates a path for that record's show action.

redirect_to @product

Since @product is a {Product} instance, Rails pluralizes the model name and includes the object's ID in the path to produce "/products/2" for the redirect.

When {save} is unsuccessful and the record wasn't valid, we want to re-render the form so the user can fix the invalid data. In the {else} clause, we tell Rails to render :new. Rails knows we're in the {Products} controller, so it should render app/views/products/new.html.erb. Since we've set the @product variable in {create}, we can render that template and the form will be populated with our {Product} data even though it wasn't able to be saved in the database.

We also set the HTTP status to 422 Unprocessable Entity to tell the browser this POST request failed and to handle it accordingly.

Editing Products

The process of editing records is very similar to creating records. Instead of {new} and {create} actions, we will have {edit} and {update}.

Let's implement them in the controller with the following:

# app/controllers/products_controller.rb
class ProductsController < ApplicationController
  def index
    @products = Product.all
  end

  def show
    @product = Product.find(params[:id])
  end

  def new
    @product = Product.new
  end

  def create
    @product = Product.new(product_params)
    if @product.save
      redirect_to @product
    else
      render :new, status: :unprocessable_entity
    end
  end

  def edit
    @product = Product.find(params[:id])
  end

  def update
    @product = Product.find(params[:id])
    if @product.update(product_params)
      redirect_to @product
    else
      render :edit, status: :unprocessable_entity
    end
  end

  private
    def product_params
      params.expect(product: [ :name ])
    end
end

Extracting Partials

We've already written a form for creating new products. Wouldn't it be nice if we could reuse that for edit and update? We can, using a feature called "partials" that allows you to reuse a view in multiple places.

We can move the form into a file called app/views/products/_form.html.erb. The filename starts with an underscore to denote this is a partial.

We also want to replace any instance variables with a local variable, which we can define when we render the partial. We'll do this by replacing @product with {product}.

Let's also display any errors from the form submission inside the form.

<%# app/views/products/_form.html.erb %>
<%= form_with model: product do |form| %>
  <% if form.object.errors.any? %>
    <p class="error"><%= form.object.errors.full_messages.first %></p>
  <% end %>

  <div>
    <%= form.label :name %>
    <%= form.text_field :name %>
  </div>

  <div>
    <%= form.submit %>
  </div>
<% end %>

TIP: Using local variables allows partials to be reused multiple times on the same page with a different value each time. This comes in handy rendering lists of items like an index page.

To use this partial in our app/views/products/new.html.erb view, we can replace the form with a render call:

<%# app/views/products/new.html.erb %>
<h1>New product</h1>

<%= render "form", product: @product %>
<%= link_to "Cancel", products_path %>

The edit view becomes almost the exact same thing thanks to the form partial. Let's create app/views/products/edit.html.erb with the following:

<%# app/views/products/edit.html.erb %>
<h1>Edit product</h1>

<%= render "form", product: @product %>
<%= link_to "Cancel", @product %>

To learn more about view partials, check out the Action View Guide.

Now we can add an Edit link to app/views/products/show.html.erb:

<%# app/views/products/show.html.erb %>
<h1><%= @product.name %></h1>

<%= link_to "Back", products_path %>
<%= link_to "Edit", edit_product_path(@product) %>

Before Actions

Since {edit} and {update} require an existing database record like {show} we can deduplicate this into a {before_action}.

A {before_action} allows you to extract shared code between actions and run it before the action. In the above controller code, @product = {Product.find}(params) is defined in three different methods. Extracting this query to a before action called {set_product} cleans up our code for each action.

This is a good example of the DRY (Don't Repeat Yourself) philosophy in action.

# app/controllers/products_controller.rb
class ProductsController < ApplicationController
  before_action :set_product, only: %i[ show edit update ]

  def index
    @products = Product.all
  end

  def show
  end

  def new
    @product = Product.new
  end

  def create
    @product = Product.new(product_params)
    if @product.save
      redirect_to @product
    else
      render :new, status: :unprocessable_entity
    end
  end

  def edit
  end

  def update
    if @product.update(product_params)
      redirect_to @product
    else
      render :edit, status: :unprocessable_entity
    end
  end

  private
    def set_product
      @product = Product.find(params[:id])
    end

    def product_params
      params.expect(product: [ :name ])
    end
end

Deleting Products

The last feature we need to implement is deleting products. We will add a {destroy} action to our {ProductsController} to handle DELETE /products/:id requests.

Adding {destroy} to before_action :set_product lets us set the @product instance variable in the same way we do for the other actions.

# app/controllers/products_controller.rb
class ProductsController < ApplicationController
  before_action :set_product, only: %i[ show edit update destroy ]

  def index
    @products = Product.all
  end

  def show
  end

  def new
    @product = Product.new
  end

  def create
    @product = Product.new(product_params)
    if @product.save
      redirect_to @product
    else
      render :new, status: :unprocessable_entity
    end
  end

  def edit
  end

  def update
    if @product.update(product_params)
      redirect_to @product
    else
      render :edit, status: :unprocessable_entity
    end
  end

  def destroy
    @product.destroy
    redirect_to products_path
  end

  private
    def set_product
      @product = Product.find(params[:id])
    end

    def product_params
      params.expect(product: [ :name ])
    end
end

To make this work, we need to add a Delete button to app/views/products/show.html.erb:

<%# app/views/products/show.html.erb %>
<h1><%= @product.name %></h1>

<%= link_to "Back", products_path %>
<%= link_to "Edit", edit_product_path(@product) %>
<%= button_to "Delete", @product, method: :delete, data: { turbo_confirm: "Are you sure?" } %>

{button_to} generates a form with a single button in it with the "Delete" text. When this button is clicked, it submits the form which makes a {DELETE} request to /products/:id which triggers the {destroy} action in our controller.

The {turbo_confirm} data attribute tells the Turbo JavaScript library to ask the user to confirm before submitting the form. We'll dig more into that shortly.

Adding Authentication

Anyone can edit or delete products which isn't safe. Let's add some security by requiring a user to be authenticated to manage products.

Rails comes with an authentication generator that we can use. It creates User and Session models and the controllers and views necessary to login to our application.

Head back to your terminal and run the following command:

$ bin/rails generate authentication

Then migrate the database to add the User and Session tables.

$ bin/rails db:migrate

Open the Rails console to create a User.

$ bin/rails console

Use {User.create!} method to create a User in the Rails console. Feel free to use your own email and password instead of the example.

store(dev)> <code>User.create!</code> email_address: "you@example.org", password: "s3cr3t", password_confirmation: "s3cr3t"

Restart your Rails server so it picks up the {bcrypt} gem added by the generator. BCrypt is used for securely hashing passwords for authentication.

$ bin/rails server

When you visit any page, Rails will prompt for a username and password. Enter the email and password you used when creating the User record.

Try it out by visiting http://localhost:3000/products/new

If you enter the correct username and password, it will allow you through. Your browser will also store these credentials for future requests so you don't have to type it in every page view.

Adding Log Out

To log out of the application, we can add a button to the top of app/views/layouts/application.html.erb. This layout is where you put HTML that you want to include in every page like a header or footer.

Add a small