Skip to content

Latest commit

 

History

History
1294 lines (1053 loc) · 37.4 KB

scaffolding-and-rest.adoc

File metadata and controls

1294 lines (1053 loc) · 37.4 KB

Scaffolding and REST

Introduction

Scaffolding means purely and simply that a basic scaffold for an application is created with a generator. This scaffold not only contains the model but also a simple Web GUI (views) and of course a controller. The programming paradigm used for this is REST (Representational State Transfer).

You can find a definition of REST at wikipedia.org/wiki/Representational_state_transfer. My super short version: the inventor Roy Fielding described in 2000 how you can access data with a simple set of rules within the concept of CRUD and the specification of the Hypertext Transfer Protocol (HTTP). CRUD is the abbreviation for Create (SQL: INSERT), Read (SQL: SELECT), Update (SQL: UPDATE) and Delete (SQL: Delete). This created URLs that are easy to read for humans and have a certain logic. In this chapter, you will see examples showing the individual paths for the different CRUD functions.

I think the greatest frustration with Rails arises regularly from the fact that many beginners use scaffolding to get quick results without having proper basic knowledge of Ruby and without knowing what ActiveRecord is. They don’t know what to do next. Fortunately, you have worked your way through the chapters "Ruby Basics", "First Steps with Rails" and "ActiveRecord", so you will be able to understand and use scaffolding straight away.

Redirects and Flash Messages

Scaffolding uses redirects and flash messages. So we have to make a little detour first to understand scaffolding.

Redirects

The name says it all, really: redirects are commands that you can use within the controller to skip, i.e. redirect, to other web pages.

Note
A redirect returns to the browser the response 302 Moved with the new target. So each redirect does a roundtrip to the browser and back.

Let’s create a new Rails project for a suitable example:

$ rails new redirect_example
[...]
$ cd redirect_example

Before we can redirect, we need a controller with at least two different methods. Off we go with a ping pong example:

$ rails generate controller Game ping pong
Running via Spring preloader in process 51759
      create  app/controllers/game_controller.rb
       route  get 'game/pong'
       route  get 'game/ping'
      invoke  erb
      create    app/views/game
      create    app/views/game/ping.html.erb
      create    app/views/game/pong.html.erb
      invoke  test_unit
      create    test/controllers/game_controller_test.rb
      invoke  helper
      create    app/helpers/game_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/game.coffee
      invoke    scss
      create      app/assets/stylesheets/game.scss

The controller app/controllers/game_controller.rb has the following content:

app/controllers/game_controller.rb
class GameController < ApplicationController
  def ping
  end

  def pong
  end
end

Now for the redirect: how can we achieve that we get immediately redirected to the method pong when we go to http://localhost:3000/game/ping? Easy, you will say, we just change the route in config/routes.rb. And you are right. So we don’t necessarily need a redirect. But if we want to process something else in the method ping before redirecting, then this is only possible by using a redirect_to in the controller app/controllers/game_controller.rb:

app/controllers/game_controller.rb
class GameController < ApplicationController
  def ping
   logger.info '+++  Example  +++'
   redirect_to game_pong_path
  end

  def pong
  end
end

But what is game_pong_path? Let’s have a look a the routes generated for this Rails application:

$ rails routes
   Prefix Verb URI Pattern          Controller#Action
game_ping GET  /game/ping(.:format) game#ping
game_pong GET  /game/pong(.:format) game#pong
Note

As you can see, the route to the action ping of the controller GameController now gets the name game_ping (see beginning of the line). We could also write the redirect like this:

redirect_to :action => 'pong'

I will explain the details and the individual options of the redirect later in the context of each specific case. For now, you just need to know that you can redirect not just to another method, but also to another controller or an entirely different web page.

When we try to go to http://localhost:3000/game/ping we are automatically redirected to http://localhost:3000/game/pong and in the log output we see this:

Started GET "/game/ping" for 127.0.0.1 at 2015-04-15 17:50:04 +0200
Processing by GameController#ping as HTML
+++  Example  +++
Redirected to http://localhost:3000/game/pong
Completed 302 Found in 14ms (ActiveRecord: 0.0ms)


Started GET "/game/pong" for 127.0.0.1 at 2015-04-15 17:50:04 +0200
Processing by GameController#pong as HTML
  Rendered game/pong.html.erb within layouts/application (2.1ms)
Completed 200 OK in 2128ms (Views: 2127.4ms | ActiveRecord: 0.0ms)

redirect_to :back

If you want to redirect the user of your web application to the page he has just been you can use redirect_to :back. This is very useful in a scenario where your user first has to login to get access to a specific page.

Flash Messages

In my eyes, the term “flash messages” is somewhat misleading. Almost anyone would associate the term “Flash” with more or less colorful web pages that were implemented with the Adobe Shockwave Flash Plug-in. But in Ruby on Rails, flash messages are something completely different. They are messages that are displayed, for example on the new page after a redirect (see section Redirects).

Flash messages are good friends with redirects. The two often work together in a team to give the user feedback on an action he just carried out. A typical example of a flash message is the system feedback when a user has logged in. Often the user is redirected back to the original page and gets the message “You are now logged in.”

As an example, we are once more constructing the ping pong scenario from section "Redirects":

$ rails new pingpong
      [...]
$ cd pingpong
$ rails generate controller Game ping pong
      [...]

We fill the app/controllers/game_controller.rb with the following content:

app/controllers/game_controller.rb
class GameController < ApplicationController
  def ping
   redirect_to game_pong_path, notice: 'Ping-Pong!'
  end

  def pong
  end
end

Now we start the Rails web server with rails server and use the browser to go to http://localhost:3000/game/ping. We are redirected from ping to pong. But the flash message "Ping-Pong!" is nowhere to be seen. We first need to expand app/views/layouts/application.html.erb:

app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>RedirectExample</title>
    <%= csrf_meta_tags %>

    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <% flash.each do |name, message| %>
      <p><i><%= "#{name}: #{message}" %></i></p>
    <% end %>
    <%= yield %>
  </body>
</html>

Now we see the flash message at the top of the page when we go to http://localhost:3000/game/ping in the browser:

flash pong notice

If we go to http://localhost:3000/game/pong we still see the normal Pong page. But if we go to http://localhost:3000/game/ping we are redirected to the Pong page and then the flash message is displayed at the top.

Tip
If you do not see a flash message that you were expecting, first check in the view to see if the flash message is output there.

Different Types of Flash Message

Flash messages are automagically passed to the view in a hash. By default, there are three different types: error, warning and notice. You can also invent your own category and then get it in the view later.

You can set a flash message by writing the hash directly too:

flash[:notice] = 'Ping-Pong!'

Please have a look at the official documentation at http://guides.rubyonrails.org/action_controller_overview.html#the-flash for more information.

Why Are There Flash Messages At All?

You may wonder why there are flash messages in the first place. Couldn’t you just build them yourself if you need them? Yes, indeed. But flash messages have the advantage that they offer a defined approach that is the same for any programmer. So you don’t need to start from scratch every single time you need one.

Generating a Scaffold

Let’s first use scaffolding to create a list of products for an online shop. First, we need to create a new Rails application:

$ rails new scaffold-shop
  [...]
$ cd scaffold-shop

Let’s look at the scaffolding options:

$ rails generate scaffold
Usage:
  rails generate scaffold NAME [field[:type][:index] field[:type][:index]]
  [options]

[...]

Examples:
    `rails generate scaffold post`
    `rails generate scaffold post title body:text published:boolean`
    `rails generate scaffold purchase amount:decimal tracking_id:integer:uniq`
    `rails generate scaffold user email:uniq password:digest`

I’ll keep it short: for our current state of knowledge, we can use rails generate scaffold just like rails generate model. Let’s create the scaffold for the products:

$ rails generate scaffold product name 'price:decimal{7,2}'
Running via Spring preloader in process 52261
      invoke  active_record
      create    db/migrate/20170323161553_create_products.rb
      create    app/models/product.rb
      invoke    test_unit
      create      test/models/product_test.rb
      create      test/fixtures/products.yml
      invoke  resource_route
       route    resources :products
      invoke  scaffold_controller
      create    app/controllers/products_controller.rb
      invoke    erb
      create      app/views/products
      create      app/views/products/index.html.erb
      create      app/views/products/edit.html.erb
      create      app/views/products/show.html.erb
      create      app/views/products/new.html.erb
      create      app/views/products/_form.html.erb
      invoke    test_unit
      create      test/controllers/products_controller_test.rb
      invoke    helper
      create      app/helpers/products_helper.rb
      invoke      test_unit
      invoke    jbuilder
      create      app/views/products/index.json.jbuilder
      create      app/views/products/show.json.jbuilder
      create      app/views/products/_product.json.jbuilder
      invoke  test_unit
      create    test/system/products_test.rb
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/products.coffee
      invoke    scss
      create      app/assets/stylesheets/products.scss
      invoke  scss
      create    app/assets/stylesheets/scaffolds.scss

As you can see, rails generate scaffold has already created the model. So we can directly call rails db:migrate:

$ rails db:migrate
== 20151218150127 CreateProducts: migrating ===================================
-- create_table(:products)
   -> 0.0023s
== 20151218150127 CreateProducts: migrated (0.0024s) ==========================

Let’s create the first six products in the db/seeds.rb.

Product.create(name: 'Apple', price: 1)
Product.create(name: 'Orange', price: 1)
Product.create(name: 'Pineapple', price: 2.4)
Product.create(name: 'Marble cake', price: 3)

Populate with the example data:

$ rails db:seed

The Routes

rails generate scaffold has created a route (more on this later in the chapter "Routes"), a controller and several views for us.

We could also have done all of this manually. Scaffolding is merely an automatism that does the work for us for some basic things. This is assuming that you always want to view, create and delete records.

Without diving too deeply into the topic routes, let’s just have a quick look at the available routes for our example. You need to run rails routes:

$ rails routes
      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

These are all the routes and consequently URLs available in this Rails application. All routes invoke actions (in other words, methods) in the ProductsController.

The Controller

Now it’s about time we had a look at the file app/controllers/products_controller.rb. Scaffold automatically creates the methods index, show, new, create, update and destroy. These methods or actions are called by the routes.

Here is the content of app/controllers/products_controller.rb

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

  # GET /products
  # GET /products.json
  def index
    @products = Product.all
  end

  # GET /products/1
  # GET /products/1.json
  def show
  end

  # GET /products/new
  def new
    @product = Product.new
  end

  # GET /products/1/edit
  def edit
  end

  # POST /products
  # POST /products.json
  def create
    @product = Product.new(product_params)

    respond_to do |format|
      if @product.save
        format.html { redirect_to @product, notice: 'Product was successfully created.' }
        format.json { render :show, status: :created, location: @product }
      else
        format.html { render :new }
        format.json { render json: @product.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /products/1
  # PATCH/PUT /products/1.json
  def update
    respond_to do |format|
      if @product.update(product_params)
        format.html { redirect_to @product, notice: 'Product was successfully updated.' }
        format.json { render :show, status: :ok, location: @product }
      else
        format.html { render :edit }
        format.json { render json: @product.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /products/1
  # DELETE /products/1.json
  def destroy
    @product.destroy
    respond_to do |format|
      format.html { redirect_to products_url, notice: 'Product was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_product
      @product = Product.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def product_params
      params.require(:product).permit(:name, :price)
    end
end

Let us take a moment and go through this controller.

set_product

A before_action calls a private method to set an instance variable @product for the actions :show, :edit, :update and :destroy. That DRYs it up nicely:

before_action :set_product, only: [:show, :edit, :update, :destroy]

[...]

private
  # Use callbacks to share common setup or constraints between actions.
  def set_product
    @product = Product.find(params[:id])
  end
[...]

index

The index method sets the instance variable @products. It contains the result of Product.all.

# GET /products
# GET /products.json
def index
  @products = Product.all
end

show

The show method doesn’t do anything. set_product before_action already set the instance variable @product. So there is not more to do.

# GET /products/1
# GET /products/1.json
def show
end
new

The new method creates a new instance of Product and saves it in the instance variable @product.

# GET /products/new
def new
  @product = Product.new
end

edit

The edit method doesn’t do anything. the set_product before_action already set the instance variable @product. So there is not more to do.

# GET /products/1/edit
def edit
end

create

The create method uses Product.new to create a new instance of Product and stores it in @product. The private method product_params is used to filter the trusted parameters with a white list. When @product was successfully saved a redirect to the show action is initiated for html requests. If a validation error occurred the new action will be rendered.

# POST /products
# POST /products.json
def create
  @product = Product.new(product_params)

  respond_to do |format|
    if @product.save
      format.html { redirect_to @product, notice: 'Product was successfully created.' }
      format.json { render :show, status: :created, location: @product }
    else
      format.html { render :new }
      format.json { render json: @product.errors, status: :unprocessable_entity }
    end
  end
end

[...]

# Never trust parameters from the scary internet, only allow the white list through.
def product_params
  params.require(:product).permit(:name, :price)
end

update

The update method tries to update @product with the product_params. The private method product_params is used to filter the trusted parameters with a white list. When @product was successfully updated a redirect to the show action is initiated for html requests. If a validation error occured the edit action will be rendered.

# PATCH/PUT /products/1
# PATCH/PUT /products/1.json
def update
  respond_to do |format|
    if @product.update(product_params)
      format.html { redirect_to @product, notice: 'Product was successfully updated.' }
      format.json { render :show, status: :ok, location: @product }
    else
      format.html { render :edit }
      format.json { render json: @product.errors, status: :unprocessable_entity }
    end
  end
end

[...]

# Never trust parameters from the scary internet, only allow the white list through.
def product_params
  params.require(:product).permit(:name, :price)
end

destroy

The destroy method destroys @product and redirects an html request to the index action.

# DELETE /products/1
# DELETE /products/1.json
def destroy
  @product.destroy
  respond_to do |format|
    format.html { redirect_to products_url, notice: 'Product was successfully destroyed.' }
    format.json { head :no_content }
  end
end

The Views

Now we start the Rails web server:

$ rails server
=> Booting Puma
=> Rails 5.1.0 application starting in development on http://localhost:3000
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 3.8.2 (ruby 2.4.0-p0), codename: Sassy Salamander
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://0.0.0.0:3000
Use Ctrl-C to stop

Now a little drum roll …​ dramatic suspense …​ launch the web browser and go to the URL http://localhost:3000/products. You can see the list of products as simple web page.

products index
Figure 1. Products index

If you now click the link New Product, you will see an input form for a new record:

product new
Figure 2. Products new

Use your browser’s Back button to go back and click on the Show link in the first line. You will then see the following page:

product show
Figure 3. Products show

If you now click Edit, you will see the editing view for this record:

product edit
Figure 4. Products edit

And if you click Destroy on the Index page, you can delete a record after confirming the message that pops up. Isn’t that cool?! Within less than 10 minutes, you have written a Web application that allows you to *c*reate, *r*ead/*r*etrieve, *u*pdate and *d*elete/*d*estroy records CRUD. That is the scaffolding magic. You can save a lot of time.

Where Are the Views?

You can probably guess, but let’s have a look at the directory app/views/products anyway:

$ tree app/views/products/
app/views/products/
├── _form.html.erb
├── _product.json.jbuilder
├── edit.html.erb
├── index.html.erb
├── index.json.jbuilder
├── new.html.erb
├── show.html.erb
└── show.json.jbuilder

There are two different file extensions. The html.erb is for HTML requests and the json.jbuilder is for JSON requests.

For index, edit, new and show the corresponding views are located there. As new and edit both require a form for editing the data, this is stored in the partial _form.html.erb in accordance with the principle of DRY (*D*on’t *R*epeat *Y*ourself) and integrated in new.html.erb and edit.html.erb with a <%= render 'form' %>.

Let’s open the file app/views/products/index.html.erb:

app/views/products/index.html.erb
<p id="notice"><%= notice %></p>

<h1>Products</h1>

<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Price</th>
      <th colspan="3"></th>
    </tr>
  </thead>

  <tbody>
    <% @products.each do |product| %>
      <tr>
        <td><%= product.name %></td>
        <td><%= product.price %></td>
        <td><%= link_to 'Show', product %></td>
        <td><%= link_to 'Edit', edit_product_path(product) %></td>
        <td><%= link_to 'Destroy', product, method: :delete, data: { confirm: 'Are you sure?' } %></td>
      </tr>
    <% end %>
  </tbody>
</table>

<br>

<%= link_to 'New Product', new_product_path %>

You are now an old hand when it comes to ERB, so you’ll be able to read and understand the code without any problems.

In the views generated by the scaffold generator, you first came across the helper link_to. This creates <a hre …​> links. You can of course also enter a link manually via <a href="…​"> in the erb, but for links within a Rails project, link_to is more practical, because you can use the names of the routes as a target. The code becomes much easier to read. In the above example, there are the following routes:

$ rails routes
      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

The first part of this route is the name of the route. With a new call, this is new_product. A link to new_product looks like this in the erb code (you can see it at the end of the file app/views/products/index.html.erb):

<%= link_to 'New Product', new_product_path %>

In the HTML code of the generated page (http://localhost:3000/products) you can see the result:

<%= link_to 'New Product', new_product_path %>

With link_to you can also link to resources within a RESTful resource. Again, you can find examples for this in app/views/products/index.html.erb. In the table, a show, an edit and a destroy link is rendered for each product:

<tbody>
  <% @products.each do |product| %>
    <tr>
      <td><%= product.name %></td>
      <td><%= product.price %></td>
      <td><%= link_to 'Show', product %></td>
      <td><%= link_to 'Edit', edit_product_path(product) %></td>
      <td><%= link_to 'Destroy', product, method: :delete, data: { confirm: 'Are you sure?' } %></td>
    </tr>
  <% end %>
</tbody>

From the resource and the selected route, Rails automatically determines the required URL and the required HTTP verb (in other words, whether it is a POST, GET, PUT or DELETE). For index and show calls, you need to observe the difference between singular and plural. link_to 'Show', product links to a single record and link_to 'Show', products_path links to the index view.

Whether the name of the route is used with or without the suffix _path in link_to depends on whether Rails can `derive'' the route from the other specified information. If only one object is specified (in our example, the variable `product), then Rails automatically assumes that it is a show route.

Examples:

ERD-Code Explanation

link_to 'Show', Product.first

Link to the first product.

link_to 'New Product', new_product_path

Link to the Web interface where a new product can be created.

link_to 'Edit', edit_product_path(Product.first)

Link to the form where the first product can be edited.

link_to 'Destroy', Product.first, method: :delete

Link to deleting the first product.

form_for

In the partial used by new and edit, app/views/products/_form.html.erb, you will find the following code for the product form:

app/views/products/_form.html.erb
<%= form_with(model: product, local: true) do |f| %>
  <% if product.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(product.errors.count, "error") %> prohibited this product from being saved:</h2>

      <ul>
      <% product.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :name %>
    <%= f.text_field :name %>
  </div>

  <div class="field">
    <%= f.label :price %>
    <%= f.text_field :price %>
  </div>

  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>

In a block, the helper form_for takes care of creating the HTML form via which the user can enter the data for the record or edit it. If you delete a complete <div class="field"> element here, this can no longer be used for input in the web interface. I am not going to comment on all possible form field variations at this point. The most frequently used ones will appear in examples later on and be explained then (if they are not self-explanatory).

Note
You can find an overview of all form helpers at http://guides.rubyonrails.org/form_helpers.html

When using validations in the model, any validation errors that occur are displayed in the following code at the head of the form:

<% if product.errors.any? %>
  <div id="error_explanation">
    <h2><%= pluralize(product.errors.count, "error") %> prohibited this product from being saved:</h2>

    <ul>
    <% product.errors.full_messages.each do |message| %>
      <li><%= message %></li>
    <% end %>
    </ul>
  </div>
<% end %>

Let’s add a small validation to the app/models/product.rb model:

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

When ever somebody wants to save a product which doesn’t have a name Rails will show this Flash Error:

product error flash
Figure 5. Products error flash

Access via JSON

By default, Rails’ scaffolding generates not just access via HTML for human users, but also a direct interface for machines. The same methods index, show, new, create, update and destroy can be called via this interface, but in a format that is easier to read for machines. As an example, we will demonstrate the index action via which all data can be read in one go. With the same idea, data can be removed (destroy) or edited (update).

JSON (see wikipedia.org/wiki/Json) seems to be the new cool kid. So we use JSON.

If you do not require machine-readable access to data, you can remove these lines in the file Gemfile (followed by the command bundle).

Gemfile
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem 'jbuilder', '~> 2.5'

Of course you can delete the format.json lines manually too. But please don’t forget to delete the JSON view files too.

JSON as Default

Right at the beginning of app/controllers/products_controller.rb you will find the entry for the index action:

app/controllers/products_controller.rb
# GET /products
# GET /products.json
def index
  @products = Product.all
end

The code is straightforward. In the instance variable @products, all products are saved. The view app/views/products/index.json.jbuilder contains the following code to render the JSON:

app/views/products/index.json.jbuilder
json.array! @products, partial: 'products/product', as: :product

It renders the partial _product.json.jbuilder:

app/views/products/_product.json.jbuilder
json.extract! product, :id, :name, :price, :created_at, :updated_at
json.url product_url(product, format: :json)

You can use your browser to fetch the JSON output. Just open http://localhost:3000/products.json and view the result. I installed a JSON view extension in my Chrome browser to get a nicer format.

products index json
Figure 6. Products index json

If you do not want the JSON output, you need to delete the json.jbuilder files.

JSON and XML Together

If you ever need a JSON and XML interface in a Rails application, you just need to specify both variants in the controller in the block respond_to. Here is an example with the app/controllers/products_controller.rb in the index action:

app/controllers/products_controller.rb
# GET /products
# GET /products.json
# GET /products.xml
def index
  @products = product.all

  respond_to do |format|
    format.html # index.html.erb
    format.json { render json: @products }
    format.xml { render xml: @products }
  end
end

When Should You Use Scaffolding?

You should never use scaffolding just for the sake of it. There are Rails developers who never use scaffolding and always build everything manually. I find scaffolding quite useful for quickly getting into a new project. But it is always just the beginning.

Example for a Minimal Project

Let’s assume we need a web page quickly with which we can list products and represent them individually. But we do not require an editing or deleting function. In that case, a large part of the code created via scaffold would be useless and have to be deleted. Let’s try it out as follows:

$ rails new read-only-shop
  [...]
$ cd read-only-shop
$ rails generate scaffold product name 'price:decimal{7,2}'
  [...]
$ rails db:migrate
  [...]

Now create the db/seeds.rb with some demo products:

db/seeds.rb
Product.create(name: 'Apple', price: 1)
Product.create(name: 'Orange', price: 1)
Product.create(name: 'Pineapple', price: 2.4)
Product.create(name: 'Marble cake', price: 3)

And populate it with this data:

$ rails db:seed

As we only need index and show, we should delete the not required views:

$ rm app/views/products/_form.html.erb
$ rm app/views/products/new.html.erb
$ rm app/views/products/edit.html.erb

The json.jbuilder views are not needed either:

$ rm app/views/products/*.json.jbuilder

The file app/controllers/products_controller.rb can be simplified with an editor. It should look like this:

app/controllers/products_controller.rb
class ProductsController < ApplicationController
  before_action :set_product, only: [:show]

  # GET /products
  # GET /products.json
  def index
    @products = Product.all
  end

  # GET /products/1
  # GET /products/1.json
  def show
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_product
      @product = Product.find(params[:id])
    end
end

We only need the routes for index and show. Please open the file config/routes.rb and edit it as follows:

config/routes.rb
Rails.application.routes.draw do
  resources :products, only: [:index, :show]
end

A rails routes shows us that really only index and show are routed now:

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

If we now start the server rails server and go to the URL http://localhost:3000/products, we get an error message.

products index json
Figure 7. Products index json

The same message will be displayed in the log:

$ rails server
=> Booting Puma
=> Rails 5.1.0 application starting in development on http://localhost:3000
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 3.8.2 (ruby 2.4.0-p0), codename: Sassy Salamander
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://0.0.0.0:3000
Use Ctrl-C to stop
Started GET "/products" for 127.0.0.1 at 2017-03-23 17:47:43 +0100
   (0.2ms)  SELECT "schema_migrations"."version" FROM "schema_migrations" ORDER BY "schema_migrations"."version" ASC
Processing by ProductsController#index as HTML
  Rendering products/index.html.erb within layouts/application
  Product Load (0.2ms)  SELECT "products".* FROM "products"
  Rendered products/index.html.erb within layouts/application (126.3ms)
Completed 500 Internal Server Error in 149ms (ActiveRecord: 0.7ms)



ActionView::Template::Error (undefined method `edit_product_path' for #<#<Class:0x007f98eb1e8148>:0x007f98ea6620d0>
Did you mean?  edit_polymorphic_path):
    17:         <td><%= product.name %></td>
    18:         <td><%= product.price %></td>
    19:         <td><%= link_to 'Show', product %></td>
    20:         <td><%= link_to 'Edit', edit_product_path(product) %></td>
    21:         <td><%= link_to 'Destroy', product, method: :delete, data: { confirm: 'Are you sure?' } %></td>
    22:       </tr>
    23:     <% end %>

app/views/products/index.html.erb:20:in `block in _app_views_products_index_html_erb___4554496912710881403_70147378203280'
app/views/products/index.html.erb:15:in `_app_views_products_index_html_erb___4554496912710881403_70147378203280'

The error message states that we call an undefined method edit_product_path in the view app/views/products/index.html.erb. As we only route index and show now, there are no more edit, destroy or new methods any more. So we need to adapt the file app/views/products/index.html.erb in the editor as follows:

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

<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Price</th>
      <th></th>
    </tr>
  </thead>

  <tbody>
    <% @products.each do |product| %>
      <tr>
        <td><%= product.name %></td>
        <td><%= product.price %></td>
        <td><%= link_to 'Show', product %></td>
      </tr>
    <% end %>
  </tbody>
</table>

And while we are at it, we also edit the app/views/products/show.html.erb accordingly:

app/views/products/show.html.erb
<p>
  <strong>Name:</strong>
  <%= @product.name %>
</p>

<p>
  <strong>Price:</strong>
  <%= @product.price %>
</p>

<%= link_to 'Back', products_path %>

Now our application is finished. Start the Rails server with rails server and open the URL http://localhost:3000/products in the browser.

read only products index
Figure 8. ReadOnlyProducts index
Note
In this example, I am not commenting on the required changes in the tests, as this is not an exercise for test driven development but meant to demonstrate a way of working with scaffolding. TDD developers will quickly be able to adapt the tests.

Conclusion

Have a go and try it out. Try working with scaffolds one time and without them the next. Then you will soon get a feel for whether it fits into your working flow or not. I find that scaffolding makes my work much easier for standard applications.