How to Build CRUD Applications Quickly with Rails
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.yml
On the Rails Guide, you can check a detailed explanation of the application directory structure.
cd
to the store directory:
$ cd store
And start the Rails server to ensure everything is working:
$ bin/rails s
Visit 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.yml
This creates:
- A migration file for the
database tableproducts
- A
model file.Product
- Test files
We can use the generate command without parameters, like this:
$ bin/rails generate model Product
It 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 --help
Next, run the migration to create the
table in your database:products
$ 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_unit
This 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 show
In this case, the files will contain the methods index
and show
.
Let's add basic CRUD actions to the
by editingProductsController
file: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, 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
end
Define routes for the ProductsController
by adding resources :products
to the
, so it looks like:config/routes.rb
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
end
For 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 class="field">
<%= form.label :name %>
<%= form.text_field :name %>
</div>
<div class="field">
<%= form.label :description %>
<%= form.text_area :description %>
</div>
<div class="field">
<%= form.label :price %>
<%= form.number_field :price, step: 0.01 %>
</div>
<div class="actions">
<%= 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
route, you can start managing products:http://localhost:3000/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
model with full CRUD capabilities using a single command:Category
$ 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.jbuilder
This generates:
- A migration for the
table.categories
- A
model.Category
- Updates the routes files
- A
with all CRUD actions.CategoriesController
- 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 --help
As 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.rb
Run 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
model to establish the relationship:Product
class Product < ApplicationRecord
belongs_to :category
end
Update the Category
model:
class Category < ApplicationRecord
has_many :products, dependent: :destroy
end
Modify the product_params
method in
to permit ProductsController
:category_id
def product_params
params.require(:product).permit(:name, :description, :price, :category_id)
end
Add a dropdown for selecting a category in
:app/views/products/_form.html.erb
<div class="field">
<%= 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
and scaffolding for Product
. 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.Category
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.