cyberangles guide

Building a Simple Web Application with Ruby on Rails

Ruby on Rails (often called "Rails") is a powerful, open-source web application framework built with Ruby. It follows the "convention over configuration" (CoC) principle, which means Rails makes sensible assumptions about how your code should be structured, reducing the need for manual setup. This allows developers to build full-featured web apps quickly. In this tutorial, we’ll build a **simple todo list application** with core features like creating, reading, updating, and deleting (CRUD) tasks, marking tasks as complete, and basic styling. By the end, you’ll have a working web app and a solid understanding of Rails fundamentals like MVC architecture, routes, controllers, views, and databases.

Table of Contents

  1. Prerequisites
  2. Setting Up a New Rails Application
  3. Understanding Rails MVC Architecture
  4. Creating a Data Model: The Task Resource
  5. Configuring Routes
  6. Building the Controller
  7. Creating Views with ERB Templates
  8. Adding Functionality: Marking Tasks as Complete
  9. Styling with Bootstrap
  10. Testing the Application
  11. Deploying to Heroku (Optional)
  12. Conclusion
  13. References

Prerequisites

Before we start, ensure you have the following installed:

  • Ruby: Rails is built on Ruby. Install Ruby using rbenv (recommended) or RVM. Check with ruby -v (Rails 7+ requires Ruby 3.0+).
  • Rails: Install Rails via RubyGems: gem install rails. Verify with rails -v (we’ll use Rails 7 in this tutorial).
  • Node.js & Yarn: Rails uses these for asset management. Install via NodeSource or nvm. Check with node -v and yarn -v.
  • Database: Rails defaults to SQLite (good for development). For production, we’ll use PostgreSQL later.

Setting Up a New Rails Application

Let’s create a new Rails app named todo_app. Open your terminal and run:

rails new todo_app  
cd todo_app  

This command generates a full Rails project structure with directories for models, views, controllers, routes, and more.

To confirm the setup worked, start the Rails server:

rails server  

Visit http://localhost:3000 in your browser. You should see the Rails welcome page—congrats, your app is running!

Understanding Rails MVC Architecture

Rails follows the Model-View-Controller (MVC) pattern, a design principle that separates an app into three interconnected components:

  • Model: Manages data and business logic (e.g., a Task model to store todo items).
  • View: Handles the user interface (e.g., HTML templates to display tasks).
  • Controller: Mediates between models and views (e.g., fetching tasks from the database and passing them to the view).

We’ll build our todo app by implementing each part of MVC.

Creating a Data Model: The Task Resource

A “resource” in Rails is a collection of related actions (like CRUD) for a model. For our todo app, the primary resource is Task.

Step 1: Generate a Model and Migration

Rails provides generators to automate model creation. Run this to generate a Task model with title (string), description (text), and completed (boolean) attributes:

rails generate model Task title:string description:text completed:boolean  

This creates:

  • A model file: app/models/task.rb
  • A database migration file: db/migrate/[timestamp]_create_tasks.rb

Step 2: Configure the Migration

Open the migration file (e.g., db/migrate/20240520123456_create_tasks.rb). By default, Rails sets completed:boolean to null: false, default: false (tasks start as incomplete). Verify the code:

class CreateTasks < ActiveRecord::Migration[7.0]  
  def change  
    create_table :tasks do |t|  
      t.string :title  
      t.text :description  
      t.boolean :completed, default: false, null: false  

      t.timestamps # Adds `created_at` and `updated_at` columns  
    end  
  end  
end  

Step 3: Run the Migration

Execute the migration to create the tasks table in the database:

rails db:migrate  

Step 4: Add Model Validations

Validations ensure data integrity (e.g., a task must have a title). Open app/models/task.rb and add:

class Task < ApplicationRecord  
  # Validate that title is present and not too long  
  validates :title, presence: true, length: { maximum: 100 }  
  # Description is optional but can’t be too long  
  validates :description, length: { maximum: 500 }  
end  

Configuring Routes

Routes map URLs to controller actions. Open config/routes.rb and define routes for the tasks resource. Replace the default code with:

Rails.application.routes.draw do  
  # Set the homepage to the tasks index  
  root "tasks#index"  

  # Generate CRUD routes for tasks (index, show, new, create, edit, update, destroy)  
  resources :tasks  

  # Add a custom route to mark tasks as complete  
  patch "tasks/:id/toggle_complete", to: "tasks#toggle_complete", as: :toggle_complete_task  
end  

To see all routes, run rails routes in the terminal.

Building the Controller

Controllers handle requests, interact with models, and render views. Generate a TasksController with CRUD actions:

rails generate controller Tasks index show new edit create update destroy  

This creates app/controllers/tasks_controller.rb and view templates (we’ll edit these later).

Update the Controller Actions

Open app/controllers/tasks_controller.rb and replace the generated code with:

class TasksController < ApplicationController  
  # Runs before show, edit, update, destroy to fetch the task  
  before_action :set_task, only: [:show, :edit, :update, :destroy, :toggle_complete]  

  # GET /tasks (index action: list all tasks)  
  def index  
    @tasks = Task.all.order(created_at: :desc) # Sort newest first  
  end  

  # GET /tasks/1 (show action: display a single task)  
  def show  
  end  

  # GET /tasks/new (new action: render form to create a task)  
  def new  
    @task = Task.new  
  end  

  # GET /tasks/1/edit (edit action: render form to update a task)  
  def edit  
  end  

  # POST /tasks (create action: save new task to database)  
  def create  
    @task = Task.new(task_params)  

    if @task.save  
      redirect_to @task, notice: "Task was successfully created."  
    else  
      render :new # Re-render the form if validation fails  
    end  
  end  

  # PATCH/PUT /tasks/1 (update action: save edited task)  
  def update  
    if @task.update(task_params)  
      redirect_to @task, notice: "Task was successfully updated."  
    else  
      render :edit  
    end  
  end  

  # DELETE /tasks/1 (destroy action: delete a task)  
  def destroy  
    @task.destroy  
    redirect_to tasks_url, notice: "Task was successfully destroyed."  
  end  

  # PATCH /tasks/1/toggle_complete (custom action: mark task as complete/incomplete)  
  def toggle_complete  
    @task.update(completed: !@task.completed)  
    redirect_to tasks_url, notice: "Task status updated."  
  end  

  private  
    # Fetch the task by ID (used in before_action)  
    def set_task  
      @task = Task.find(params[:id])  
    end  

    # Strong parameters: only allow title and description to be updated  
    def task_params  
      params.require(:task).permit(:title, :description)  
    end  
end  

Creating Views with ERB Templates

Views are HTML templates that display data. Rails uses ERB (Embedded Ruby) to embed Ruby code in HTML.

1. Index View (app/views/tasks/index.html.erb)

This view lists all tasks. Replace the generated code with:

<h1>Todo List</h1>  

<!-- Link to create a new task -->  
<%= link_to "Add New Task", new_task_path, class: "btn btn-primary mb-3" %>  

<!-- List all tasks -->  
<div class="tasks-list">  
  <% @tasks.each do |task| %>  
    <div class="task-card mb-3 p-3 border rounded <%= task.completed ? 'bg-success bg-opacity-10' : '' %>">  
      <h3>  
        <%= task.title %>  
        <%= "✓" if task.completed %> <!-- Show checkmark if completed -->  
      </h3>  
      <p><%= task.description %></p>  

      <!-- Action buttons -->  
      <div class="task-actions">  
        <%= link_to "View", task_path(task), class: "btn btn-sm btn-info me-2" %>  
        <%= link_to "Edit", edit_task_path(task), class: "btn btn-sm btn-warning me-2" %>  
        <%= link_to task.completed? ? "Mark Incomplete" : "Mark Complete",  
                    toggle_complete_task_path(task),  
                    method: :patch,  
                    class: "btn btn-sm #{task.completed? ? 'btn-secondary' : 'btn-success'} me-2" %>  
        <%= link_to "Delete", task_path(task),  
                    method: :delete,  
                    data: { confirm: "Are you sure?" },  
                    class: "btn btn-sm btn-danger" %>  
      </div>  
    </div>  
  <% end %>  
</div>  

<!-- If no tasks exist -->  
<% if @tasks.empty? %>  
  <p class="text-muted">No tasks yet. <%= link_to "Add your first task!", new_task_path %></p>  
<% end %>  

2. Form Partial (Reusable Form for New/Edit)

To avoid repeating code, create a partial for the task form. Create app/views/tasks/_form.html.erb:

<%= form_with model: @task, local: true do |form| %>  
  <% if @task.errors.any? %>  
    <div id="error_explanation" class="alert alert-danger">  
      <h2><%= pluralize(@task.errors.count, "error") %> prohibited this task from being saved:</h2>  
      <ul>  
        <% @task.errors.each do |error| %>  
          <li><%= error.full_message %></li>  
        <% end %>  
      </ul>  
    </div>  
  <% end %>  

  <div class="mb-3">  
    <%= form.label :title, class: "form-label" %>  
    <%= form.text_field :title, class: "form-control", required: true %>  
  </div>  

  <div class="mb-3">  
    <%= form.label :description, class: "form-label" %>  
    <%= form.text_area :description, class: "form-control", rows: 3 %>  
  </div>  

  <div class="actions">  
    <%= form.submit class: "btn btn-primary" %>  
    <%= link_to "Cancel", tasks_path, class: "btn btn-secondary ms-2" %>  
  </div>  
<% end %>  

3. New View (app/views/tasks/new.html.erb)

Render the form partial for creating a task:

<h1>New Task</h1>  
<%= render "form" %>  

4. Edit View (app/views/tasks/edit.html.erb)

Render the same form partial for editing:

<h1>Edit Task</h1>  
<%= render "form" %>  

5. Show View (app/views/tasks/show.html.erb)

Display details for a single task:

<h1><%= @task.title %></h1>  
<p><%= @task.description %></p>  
<p>  
  <strong>Status:</strong> <%= @task.completed? ? "Completed" : "Incomplete" %>  
</p>  
<p>  
  <strong>Created:</strong> <%= @task.created_at.strftime("%b %d, %Y at %I:%M %p") %>  
</p>  

<%= link_to "Back", tasks_path, class: "btn btn-secondary" %>  

Styling with Bootstrap

Let’s add Bootstrap to make the app look polished.

Step 1: Add Bootstrap to the Gemfile

Open Gemfile and add:

gem 'bootstrap', '~> 5.3.0'  
gem 'autoprefixer-rails' # For CSS prefixes  

Run bundle install to install the gems.

Step 2: Configure Assets

  • Rename app/assets/stylesheets/application.css to app/assets/stylesheets/application.bootstrap.scss (.scss extension enables Sass).

  • Open application.bootstrap.scss and replace its contents with:

    @import "bootstrap";  
  • Open app/javascript/packs/application.js and add:

    import "bootstrap"  

Step 3: Update the Layout

Open app/views/layouts/application.html.erb and wrap the yield in a Bootstrap container. Update the <body> tag:

<body>  
  <div class="container mt-4">  
    <!-- Flash messages (notices/errors) -->  
    <% if notice.present? %>  
      <div class="alert alert-success"><%= notice %></div>  
    <% end %>  
    <% if alert.present? %>  
      <div class="alert alert-danger"><%= alert %></div>  
    <% end %>  

    <%= yield %> <!-- Renders the view content -->  
  </div>  
</body>  

Testing the Application

Start the Rails server:

rails server  

Visit http://localhost:3000 and test the following:

  • Create a task (click “Add New Task”).
  • View, edit, or delete tasks.
  • Mark tasks as complete/incomplete.

Deploying to Heroku (Optional)

Deploy your app to Heroku for free hosting.

Step 1: Install Heroku CLI

Follow the Heroku installation guide.

Step 2: Prepare the App for Production

  • Add a Procfile to the root of your app (tells Heroku how to run the app):

    web: bundle exec puma -C config/puma.rb  
  • Update Gemfile to use PostgreSQL in production (replace the sqlite3 gem):

    group :development, :test do  
      gem 'sqlite3', '~> 1.4'  
    end  
    
    group :production do  
      gem 'pg', '~> 1.5' # PostgreSQL  
      gem 'rails_12factor' # For Heroku compatibility  
    end  
  • Run bundle install --without production to update dependencies.

Step 3: Deploy to Heroku

heroku login  
heroku create your-todo-app-name # Choose a unique name  
git add .  
git commit -m "Prepare for Heroku deployment"  
git push heroku main  
heroku run rails db:migrate # Run migrations on Heroku  

Visit your app at https://your-todo-app-name.herokuapp.com.

Conclusion

You’ve built a fully functional todo app with Ruby on Rails! You learned how to:

  • Set up a Rails project and use generators.
  • Implement MVC architecture (models, views, controllers).
  • Create CRUD functionality and custom actions.
  • Style with Bootstrap.
  • Deploy to Heroku.

Next steps to expand your app:

  • Add user authentication (with Devise).
  • Add due dates or priorities to tasks.
  • Implement task categories.

References