Understand the basics of Ruby on Rails by building a blog app
Akhil
Posted on August 18, 2021
Table of contents:
- What is MVC?
- Directory structure of Ruby on Rails application
- Build a blog post application that supports all CRUD operations
- How to generate models
- How to write migration files
- How to generate a controller and its views
Ruby on Rails is a web application framework written in Ruby. If you want a little bit more intro stuff, please go through Rome can be built in a day which I wrote as a part of my RoR(Ruby on Rails) series.
What is MVC?
MVC stands for Model-View-Controller. MVC is an architectural pattern that makes it easy to put a specific piece of code at a specific place.
- Model is the one that usually maps to a database table and handles data related logic.
-
View is the markup part that renders an
HTML
view. - Controller is the one that is responsible for fetching data from model objects and send it to the View.
Directory structure
The above screenshot shows the directory structure of a vanilla Rails application. Most of the application code resides under app
organized in their respective directories. The Model-View-Controller
each has its respective directory as models
, views
and directories
.
With the default configuration, let's start building a blog-post application.
System setup
For this blog, we are going to use https://ssh.cloud.google.com/
which is free and comes with Ruby and Rails installed. It also has a cloud IDE, so that we don't have to spend time setting up our local.
STEP 1: Initialize a new project
Run the following in the IDE's terminal to create a new project
$ rails new blog_app
$ cd blog_app
Once the project is created, run cd blog_app
to go to that directory. Open up the project directory in the IDE too. Now, we can try running the default app to see if everything went well.
$ rails s --port 3001
On top of the IDE, click on the web preview
as shown in the screenshot to open your application. If everything is well and good, you should see Yay! You’re on Rails! page.
STEP 2: Generate the Post model
If your server is still running, turn it down and run the following command to generate the Post
model along with the required migration file.
$ rails generate model Post
Running via Spring preloader in process 4985
invoke active_record
create db/migrate/20210817174254_create_posts.rb
create app/models/post.rb
invoke test_unit
create test/models/post_test.rb
create test/fixtures/posts.yml
Migrations are like version control for databases, allowing us to modify and share the application's database schema. 20210817174254_create_posts.rb
will be used define the schema of posts
table.
We need to store title
, and description
of posts. title
will be a string while description
will be text
column.
# db/migrate/20210817174254_create_posts.rb
class CreatePosts < ActiveRecord::Migration[6.1]
def change
create_table :posts do |t|
t.string :title
t.text :description
t.timestamps
end
end
end
By default, t.timestamps
is present in every migration file which adds created_at
and updated_at
column to tables.
Column types supported
- `primary_key` - `string` - `text` - `integer` - `bigint` - `float` - `decimal` - `numeric` - `datetime` - `time` - `date` - `binary` - `boolean`
Now, in order to reflect the migration changes in the database, we need to run the migration.
$ rails db:migrate
== 20210817174254 CreatePosts: migrating ======================================
-- create_table(:posts)
-> 0.0035s
== 20210817174254 CreatePosts: migrated (0.0036s) =============================
STEP 3: Generate PostsController and Views
When a route is hit by a client, it passes the execution to appropriate action of a controller. Let's say we open localhost:3001/users
in the browser, the router will dispatch it to index
action of users_controller
. Action are methods defined inside the controller class.
For every CRUD
operation, there has to be a route defined mapping to a controller's action. Rails by convention uses resourceful routes to them. Routes are defined in the config/routes.rb
file. Let's open up that file and add the following code:
# config/routes.rb
resources :posts
To check all the routes that the above code creates, run rails routes
.
Output of rails routes
Prefix Verb URI Pattern Controller#Action posts GET /posts(.:format) posts#index POST /posts(.:format) posts#create new_post GET /posts/new(.:format) posts#new edit_post GET /posts/:id/edit(.:format) posts#edit post GET /posts/:id(.:format) posts#show PATCH /posts/:id(.:format) posts#update PUT /posts/:id(.:format) posts#update DELETE /posts/:id(.:format) posts#destroy
Let create PostsController
with all the actions mapping to each resourceful route.
$ rails generate controller posts
Running via Spring preloader in process 1559
create app/controllers/posts_controller.rb
invoke erb
create app/views/posts
invoke test_unit
create test/controllers/posts_controller_test.rb
invoke helper
create app/helpers/posts_helper.rb
invoke test_unit
invoke assets
invoke scss
create app/assets/stylesheets/posts.scss
The posts_controller.rb
file will contain code to fetch data from the model objects and present it to the respective view. Add the following code to the controller file:
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
before_action :set_post, only: %i[ show edit update destroy ]
# GET /posts
def index
@posts = Post.all
end
# GET /posts/1
def show
end
# GET /posts/new
def new
@post = Post.new
end
# GET /posts/1/edit
def edit
end
# POST /posts
def create
@post = Post.new(post_params)
respond_to do |format|
if @post.save
redirect_to @post, notice: "Post was successfully created."
else
render :new
end
end
end
# PATCH/PUT /posts/1
def update
respond_to do |format|
if @post.update(post_params)
redirect_to @post, notice: "Post was successfully updated."
else
render :edit
end
end
end
# DELETE /posts/1
def destroy
@post.destroy
respond_to do |format|
redirect_to posts_url, notice: "Post was successfully destroyed."
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_post
@post = Post.find(params[:id])
end
# Only allow a list of trusted parameters through.
def post_params
params.require(:post).permit(:title, :description)
end
end
Every controller should inherit from ApplicationController
so that it has access to helper methods and objects. In the PostsController
, the first line is before_action :set_post, only: %i[ show edit update destroy ]
which says Rails to first invoke set_post
method whenever there is a request for show
, edit
, update
or destroy
action.
index
action assigns all the posts fetched from the database to @posts
that will be used by its respective view. But wait, there is no view created yet. Let's also create a view for index
action.
Views are named as <action_name>.html.erb
. So for the index
action, the view will be index.html.erb
. erb
is the template engine used by Rails to pass objects' values to templates, based on which the final HTML
will be built and rendered.
<%# app/views/posts/index.html.erb %>
<p id="notice"><%= notice %></p>
<h1>Posts</h1>
<table>
<thead>
<tr>
<th>Title</th>
<th>Description</th>
<th colspan="3"></th>
</tr>
</thead>
<tbody>
<% @posts.each do |post| %>
<tr>
<td><%= post.title %></td>
<td><%= post.description %></td>
<td><%= link_to 'Show', post %></td>
<td><%= link_to 'Edit', edit_post_path(post) %></td>
<td><%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %></td>
</tr>
<% end %>
</tbody>
</table>
<br>
<%= link_to 'New Post', new_post_path %>
We are using a table to print all the posts. On top, we are printing notice
that will contain the notice passed from the action.
Similarly, let's create view files for each of the actions.
-
show
<p id="notice"><%= notice %></p>
<p>
<strong>Title:</strong>
<%= @post.title %>
</p>
<p>
<strong>Description:</strong>
<%= @post.description %>
</p>
<%= link_to 'Edit', edit_post_path(@post) %> |
<%= link_to 'Back', posts_path %>
-
new
<h1>New Post</h1>
<%= form_with(model: @post) do |form| %>
<% if post.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(post.errors.count, "error") %> prohibited this post from being saved:</h2>
<ul>
<% post.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= form.label :title %>
<%= form.text_field :title %>
</div>
<div class="field">
<%= form.label :description %>
<%= form.text_area :description %>
</div>
<div class="actions">
<%= form.submit %>
</div>
<% end %>
<%= link_to 'Back', posts_path %>
-
edit
<h1>Editing Post</h1>
<%= form_with(model: @post) do |form| %>
<% if post.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(post.errors.count, "error") %> prohibited this post from being saved:</h2>
<ul>
<% post.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= form.label :title %>
<%= form.text_field :title %>
</div>
<div class="field">
<%= form.label :description %>
<%= form.text_area :description %>
</div>
<div class="actions">
<%= form.submit %>
</div>
<% end %>
<%= link_to 'Show', @post %> |
<%= link_to 'Back', posts_path %>
-
update
anddestroy
action doesn't need views as they are redirecting toshow
andlist
actions/pages respectively.
P.S: Both new.html.erb
and edit.html.erb
share a lot of common code. In that case we can extract out that common code to a partial
and then render it in other templates.
<%# app/views/posts/_form.html.erb %>
<%= form_with(model: post) do |form| %>
<% if post.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(post.errors.count, "error") %> prohibited this post from being saved:</h2>
<ul>
<% post.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= form.label :title %>
<%= form.text_field :title %>
</div>
<div class="field">
<%= form.label :description %>
<%= form.text_area :description %>
</div>
<div class="actions">
<%= form.submit %>
</div>
<% end %>
And then replace the common code in new.html.erb
and edit.html.erb
templates with just <%= render 'form', post: @post %>
.
STEP 4: Run the application
We are all set now and we just need to re-run the app as we did in the beginning. Run rails s -p3001
to run Rails on 3001 port and then open localhost:3001/posts
. You will see something like this:
🎉🎉 We are done. Try creating, editing, and doing all the fun stuff with the app.
Bonus
Our app currently accepts text and shows it as it is. We can add markdown support to make it look like an actual blog application. For that first add gem 'kramdown'
to Gemfile
. The edit the show page as following:
<% # app/views/posts/show.html.erb %>
<p id="notice"><%= notice %></p>
<p>
<strong>Title:</strong>
<%= @post.title %>
</p>
<p>
<strong>Description:</strong>
<%= sanitize Kramdown::Document.new(@post.description).to_html %>
</p>
<%= link_to 'Edit', edit_post_path(@post) %> |
<%= link_to 'Back', posts_path %>
DONE!! Now if we write the post's description in markdown
format, it will be able to render it.
What if we want the resources to be served as REST API also?
In order to do that, we need to respond with JSON response whenever it is requested. To make serializing to JSON easy, we will use a gem called jbuilder
. Let's first add gem 'jbuilder', '~> 2.7'
to the Gemfile
. Then we need to change our controller to the following:
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
before_action :set_post, only: %i[ show edit update destroy ]
# GET /posts or /posts.json
def index
@posts = Post.all
end
# GET /posts/1 or /posts/1.json
def show
end
# GET /posts/new
def new
@post = Post.new
end
# GET /posts/1/edit
def edit
end
# POST /posts or /posts.json
def create
@post = Post.new(post_params)
respond_to do |format|
if @post.save
format.html { redirect_to @post, notice: "Post was successfully created." }
format.json { render :show, status: :created, location: @post }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @post.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /posts/1 or /posts/1.json
def update
respond_to do |format|
if @post.update(post_params)
format.html { redirect_to @post, notice: "Post was successfully updated." }
format.json { render :show, status: :ok, location: @post }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @post.errors, status: :unprocessable_entity }
end
end
end
# DELETE /posts/1 or /posts/1.json
def destroy
@post.destroy
respond_to do |format|
format.html { redirect_to posts_url, notice: "Post was successfully destroyed." }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_post
@post = Post.find(params[:id])
end
# Only allow a list of trusted parameters through.
def post_params
params.require(:post).permit(:title, :description)
end
end
Now when the request comes for JSON, it will fetch the data and render the respective jbuilder
file which in turn will return serialized JSON. Let's now add the respective jbuilder
files under app/views/posts
along with the partial containing common jbuilder
code.
# app/views/posts/_post.json.jbuilder
json.extract! post, :id, :title, :description, :created_at, :updated_at
json.url post_url(post, format: :json)
# app/views/posts/index.json.jbuilder
json.array! @posts, partial: "posts/post", as: :post
# app/views/posts/show.json.jbuilder
json.partial! "posts/post", post: @post
Now, we can make fetch requests to get JSON responses from the API. Let's try running fetch('https://localhost:3001/posts/1.json').then(res => res.json()).then(json_res => console.log(json_res))
in the browser console with localhost:3001
open. You will get the json
response as configured in the show.json.jbuilder
file.
Optional resources
- Ruby/Rails naming convention: https://gist.github.com/iangreenleaf/b206d09c587e8fc6399e
- Rails guides: https://guides.rubyonrails.org/index.html
- Jbuilder: https://github.com/rails/jbuilder
- Kramdown: https://github.com/gettalong/kramdown
That is it for this. I know I am not able to cover everything but it is not even possible in one blog.
If you really liked this blog, don't forget to like it and follow me. Thanks for reading.
Posted on August 18, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.