Table of Contents
- Prerequisites
- Setting Up a New Rails Application
- Understanding Rails MVC Architecture
- Creating a Data Model: The Task Resource
- Configuring Routes
- Building the Controller
- Creating Views with ERB Templates
- Adding Functionality: Marking Tasks as Complete
- Styling with Bootstrap
- Testing the Application
- Deploying to Heroku (Optional)
- Conclusion
- 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 withrails -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 -vandyarn -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
Taskmodel 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.csstoapp/assets/stylesheets/application.bootstrap.scss(.scssextension enables Sass). -
Open
application.bootstrap.scssand replace its contents with:@import "bootstrap"; -
Open
app/javascript/packs/application.jsand 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
Procfileto the root of your app (tells Heroku how to run the app):web: bundle exec puma -C config/puma.rb -
Update
Gemfileto use PostgreSQL in production (replace thesqlite3gem):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 productionto 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.