Ruby on Rails is known for its ability to help developers build robust applications quickly and efficiently. In this tutorial, we'll walk through creating a simple CRUD (Create, Read, Update, Delete) application called "Store." This application will demonstrate Rails' built-in tools, such as scaffolding, to highlight just how much time you can save with Rails.
By the end of this guide, you'll have a functional e-commerce app with two models: Product and Category. Along the way, we'll explore different ways to create models and controllers, so you can see the options Rails provides.
Step 1: Setting Up the Rails Application
First, ensure you have Ruby and Rails installed on your system. If not, you can follow the official Rails guide or check my previous Getting Started with Rails post.
Once you're ready, open the terminal app and create a new Rails application:
$ rails new store Here are my logs from the terminal: rails new store create create README.md create Rakefile create .ruby-version create config.ru create .gitignore create .gitattributes create Gemfile run git init from "." Initialized empty Git repository in /Users/pron/Documents/store/.git/ create app create app/assets/stylesheets/application.css create app/controllers/application_controller.rb create app/helpers/application_helper.rb create app/jobs/application_job.rb create app/mailers/application_mailer.rb create app/models/application_record.rb create app/views/layouts/application.html.erb create app/views/layouts/mailer.html.erb create app/views/layouts/mailer.text.erb create app/views/pwa/manifest.json.erb create app/views/pwa/service-worker.js create app/assets/images create app/assets/images/.keep create app/controllers/concerns/.keep create app/models/concerns/.keep create bin create bin/brakeman create bin/dev create bin/rails create bin/rake create bin/rubocop create bin/setup create bin/thrust create Dockerfile create .dockerignore create bin/docker-entrypoint create .rubocop.yml create .github/workflows create .github/workflows/ci.yml create .github/dependabot.yml create config create config/routes.rb create config/application.rb create config/environment.rb create config/cable.yml create config/puma.rb create config/storage.yml create config/environments create config/environments/development.rb create config/environments/production.rb create config/environments/test.rb create config/initializers create config/initializers/assets.rb create config/initializers/content_security_policy.rb create config/initializers/cors.rb create config/initializers/filter_parameter_logging.rb create config/initializers/inflections.rb create config/initializers/new_framework_defaults_8_0.rb create config/locales create config/locales/en.yml create config/master.key append .gitignore create config/boot.rb create config/database.yml create db create db/seeds.rb create lib create lib/tasks create lib/tasks/.keep create log create log/.keep create public create public/400.html create public/404.html create public/406-unsupported-browser.html create public/422.html create public/500.html create public/icon.png create public/icon.svg create public/robots.txt create script create script/.keep create tmp create tmp/.keep create tmp/pids create tmp/pids/.keep create vendor create vendor/.keep create test/fixtures/files create test/fixtures/files/.keep create test/controllers create test/controllers/.keep create test/mailers create test/mailers/.keep create test/models create test/models/.keep create test/helpers create test/helpers/.keep create test/integration create test/integration/.keep create test/test_helper.rb create test/system create test/system/.keep create test/application_system_test_case.rb create storage create storage/.keep create tmp/storage create tmp/storage/.keep remove config/initializers/cors.rb remove config/initializers/new_framework_defaults_8_0.rb run bundle install --quiet run bundle lock --add-platform=x86_64-linux Writing lockfile to /Users/pron/Documents/store/Gemfile.lock run bundle lock --add-platform=aarch64-linux Writing lockfile to /Users/pron/Documents/store/Gemfile.lock run bundle binstubs bundler rails importmap:install apply /Users/pron/.rbenv/versions/3.3.5/lib/ruby/gems/3.3.0/gems/importmap-rails-2.1.0/lib/install/install.rb Add Importmap include tags in application layout insert app/views/layouts/application.html.erb Create application.js module as entrypoint create app/javascript/application.js Use vendor/javascript for downloaded pins create vendor/javascript create vendor/javascript/.keep Configure importmap paths in config/importmap.rb create config/importmap.rb Copying binstub create bin/importmap run bundle install --quiet rails turbo:install stimulus:install apply /Users/pron/.rbenv/versions/3.3.5/lib/ruby/gems/3.3.0/gems/turbo-rails-2.0.11/lib/install/turbo_with_importmap.rb Import Turbo append app/javascript/application.js Pin Turbo append config/importmap.rb run bundle install --quiet apply /Users/pron/.rbenv/versions/3.3.5/lib/ruby/gems/3.3.0/gems/stimulus-rails-1.3.4/lib/install/stimulus_with_importmap.rb Create controllers directory create app/javascript/controllers create app/javascript/controllers/index.js create app/javascript/controllers/application.js create app/javascript/controllers/hello_controller.js Import Stimulus controllers append app/javascript/application.js Pin Stimulus Appending: pin "@hotwired/stimulus", to: "stimulus.min.js" append config/importmap.rb Appending: pin "@hotwired/stimulus-loading", to: "stimulus-loading.js" append config/importmap.rb Pin all controllers Appending: pin_all_from "app/javascript/controllers", under: "controllers" append config/importmap.rb run bundle install --quiet run bundle binstubs kamal run bundle exec kamal init Created configuration file in config/deploy.yml Created .kamal/secrets file Created sample hooks in .kamal/hooks force .kamal/secrets force config/deploy.yml rails solid_cache:install solid_queue:install solid_cable:install create config/cache.yml create db/cache_schema.rb gsub config/environments/production.rb create config/queue.yml create config/recurring.yml create db/queue_schema.rb create bin/jobs gsub config/environments/production.rb create db/cable_schema.rb force config/cable.ymlOn the Rails Guide, you can check a detailed explanation of the application directory structure.
cd to the store directory:
$ cd storeAnd start the Rails server to ensure everything is working:
$ bin/rails sVisit http://localhost:3000 in your browser to see the default Rails welcome page.
Step 2: Creating the Product
We'll begin by creating a Product model with a Rails generate command. This will give us an opportunity to see the components Rails generates.
Run the following command to generate the model:
$ bin/rails generate model Product name:string description:text price:decimal invoke active_record create db/migrate/20241230105739_create_products.rb create app/models/product.rb invoke test_unit create test/models/product_test.rb create test/fixtures/products.ymlThis creates:
- A migration file for the products database table
- A Product model file.
- Test files
We can use the generate command without parameters, like this:
$ bin/rails generate model ProductIt creates the same files, but they will not contain any parameters (like name, description, and price), so you have to add them manually.
Check the generator documentation by running the help command like this:
$ bin/rails generate model --helpNext, run the migration to create the products table in your database:
$ bin/rails db:migrate == 20241230105739 CreateProducts: migrating =================================== -- create_table(:products) -> 0.0010s == 20241230105739 CreateProducts: migrated (0.0010s) ==========================Create Products controller
Now, create the ProductsController:
$ bin/rails generate controller Products create app/controllers/products_controller.rb invoke erb create app/views/products invoke test_unit create test/controllers/products_controller_test.rb invoke helper create app/helpers/products_helper.rb invoke test_unitThis creates:
- An empty Product controller file
- An empty products views directory
- An empty products helper file for extracting logic in our views
- Test files
This generator can be used with parameters (method names), like:
$ bin/rails generate controller Products index showIn this case, the files will contain the methods index and show.
Let's add basic CRUD actions to the ProductsController by editing app/controllers/products_controller.rb file:
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, notice: 'Product was successfully created.' else render :new 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, notice: 'Product was successfully updated.' else render :edit end end def destroy @product = Product.find(params[:id]) @product.destroy redirect_to products_url, notice: 'Product was successfully destroyed.' end private def product_params params.require(:product).permit(:name, :description, :price) end endDefine routes for the ProductsController by adding resources :products to the config/routes.rb, so it looks like:
Rails.application.routes.draw do # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. # Can be used by load balancers and uptime monitors to verify that the app is live. get "up" => "rails/health#show", as: :rails_health_check # Render dynamic PWA files from app/views/pwa/* (remember to link manifest in application.html.erb) # get "manifest" => "rails/pwa#manifest", as: :pwa_manifest # get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker # Defines the root path route ("/") # root "posts#index" resources :products endFor simplicity, you can generate basic views manually by creating individual files in the app/views/products directory. Here’s how you can do it step by step:
- Index View (app/views/products/index.html.erb):
<h1>Products</h1> <%= link_to 'New Product', new_product_path %> <ul> <% @products.each do |product| %> <li> <%= link_to product.name, product_path(product) %> — <%= number_to_currency(product.price) %> <%= link_to 'Edit', edit_product_path(product) %> | <%= link_to 'Delete', product_path(product), method: :delete, data: { turbo_method: :delete, turbo_confirm: 'Are you sure?' } %> </li> <% end %> </ul>- Show View (app/views/products/show.html.erb):
<h1><%= @product.name %></h1> <p><%= @product.description %></p> <p>Price: <%= number_to_currency(@product.price) %></p> <%= link_to 'Edit', edit_product_path(@product) %> | <%= link_to 'Back to Products', products_path %>- New (app/views/products/new.html.erb):
<h1>New Product</h1> <%= render 'form', product: @product %> <%= link_to 'Back', products_path %>- Edit (app/views/products/edit.html.erb):
<h1>Edit Product</h1> <%= render 'form', product: @product %> <%= link_to 'Back', products_path %>New and Edit Views will use the form partial app/views/products/_form.html.erb:
<%= form_with model: @product, local: true do |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 %> <div> <%= form.label :name %> <%= form.text_field :name %> </div> <div> <%= form.label :description %> <%= form.text_area :description %> </div> <div> <%= form.label :price %> <%= form.number_field :price, step: 0.01 %> </div> <div> <%= form.submit %> </div> <% end %>This allows the reuse of the same code in both places, by applying the DRY (Don’t Repeat Yourself) principle.
Now, on the http://localhost:3000/products route, you can start managing products:

By creating these files manually, you have complete control over the structure and content of each view. Alternatively, you can use a scaffold generator for guidance.
Step 3: Using Scaffolding for Category
To see how scaffolding can speed up development, let's create a Category model with full CRUD capabilities using a single command:
$ bin/rails generate scaffold Category name:string invoke active_record create db/migrate/20250102092751_create_categories.rb create app/models/category.rb invoke test_unit create test/models/category_test.rb create test/fixtures/categories.yml invoke resource_route route resources :categories invoke scaffold_controller create app/controllers/categories_controller.rb invoke erb create app/views/categories create app/views/categories/index.html.erb create app/views/categories/edit.html.erb create app/views/categories/show.html.erb create app/views/categories/new.html.erb create app/views/categories/_form.html.erb create app/views/categories/_category.html.erb invoke resource_route invoke test_unit create test/controllers/categories_controller_test.rb create test/system/categories_test.rb invoke helper create app/helpers/categories_helper.rb invoke test_unit invoke jbuilder create app/views/categories/index.json.jbuilder create app/views/categories/show.json.jbuilder create app/views/categories/_category.json.jbuilderThis generates:
- A migration for the categories table.
- A Category model.
- Updates the routes files
- A CategoriesController with all CRUD actions.
- View templates for each action for HTML and JSON request format.
- A helper file for extracting logic in our views
- Test files
Run the migration:
$ bin/rails db:migrate == 20250102092751 CreateCategories: migrating ================================= -- create_table(:categories) -> 0.0014s == 20250102092751 CreateCategories: migrated (0.0015s) ========================Now, visit http://localhost:3000/categories to manage categories.

As you can see, after two commands, you can add, edit, and delete categories using Rails's scaffolding generator. Investigate the differences between the ProductsController and CategoriesController. It's a great tool to make a prototype of the feature quickly, but as with all tools, it has cons as well, for example, it creates a lot of comments and even some unnecessary files. This could be fixed by using scaffolding options presented in the documentation:
$ bin/rails generate scaffold --helpAs you already noticed, Rails has great documentation.
Step 4: Linking Products and Categories
To associate products with categories, update the Product model to include a category_id:
Generate a migration:
$ bin/rails generate migration AddCategoryToProducts category:references invoke active_record create db/migrate/20250102093200_add_category_to_products.rbRun the migration:
$ bin/rails db:migrate == 20250102093200 AddCategoryToProducts: migrating ============================ -- add_reference(:products, :category, {:null=>false, :foreign_key=>true}) -> 0.0284s == 20250102093200 AddCategoryToProducts: migrated (0.0284s) ===================Update the Product model to establish the relationship:
class Product < ApplicationRecord belongs_to :category endUpdate the Category model:
class Category < ApplicationRecord has_many :products, dependent: :destroy endModify the product_params method in ProductsController to permit category_id:
def product_params params.require(:product).permit(:name, :description, :price, :category_id) endAdd a dropdown for selecting a category in app/views/products/_form.html.erb:
<div> <%= form.label :category_id %> <%= form.collection_select :category_id, Category.all, :id, :name, prompt: "Select a category" %> </div>Insert this code inside the form block.
Conclusion
This tutorial explored two approaches to building CRUD features in Rails: specific generators to create a model and controller for the Product and scaffolding for Category. Rails' built-in tools significantly reduce the time and effort required to set up common application features, so you can focus on building what matters most for your application.
You can expand the Store app by adding features like user authentication, shopping carts, and more.
Stay tuned! In my upcoming posts, I want to explain how to make the styling better with Tailwind CSS.