[NEW] Search for a job anonymously — check the details
Close
How to Build CRUD Applications Quickly with Rails

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 products database table
  • Product model file.
  • 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 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_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 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
end

Define 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
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 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.jbuilder

This generates:

  • A migration for the categories table.
  • Category model.
  • Updates the routes files
  • 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 --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 Product model to establish the relationship:

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 ProductsController to permit 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 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.


Avatar
Ruby on Rails Developer
Jan 4
VS Code Extensions for Ruby on Rails in 2025
VS Code Extensions help write code faster and increase productivity. Here I share some extensions that I personally use in 2025
Feb 23
Rails 8 with Solid Queue 1.1 deployed with Capistrano
How to setup Solid Queue 1.1 on Rails 8 application with Capistrano deployment
Dec 30
Getting Started with Rails: The Basics Every Developer Should Know
In this article, I’ll guide you through the initial steps for setting up your development environment, installing the necessary tools, and starting your journey as a Ruby on Rails developer.

This site uses cookies to offer you a better browsing experience.

Find out more on how we use cookies and how to change cookie preferences in our Cookies Policy.

Customize
Save Accept all cookies