DEV Community

Akhil
Akhil

Posted on • Originally published at blog.akhilgautam.me

Understand the basics of Ruby on Rails by building a blog app

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

Screenshot 2021-08-17 at 6.51.49 PM.png

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Screenshot 2021-08-17 at 11.03.11 PM.png
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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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) =============================
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 %>
Enter fullscreen mode Exit fullscreen mode

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 %>
Enter fullscreen mode Exit fullscreen mode
  • 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 %>
Enter fullscreen mode Exit fullscreen mode
  • 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 %>

Enter fullscreen mode Exit fullscreen mode
  • update and destroy action doesn't need views as they are redirecting to show and list 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 %>
Enter fullscreen mode Exit fullscreen mode

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:

Screenshot 2021-08-18 at 5.01.41 PM.png

πŸŽ‰πŸŽ‰ 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 %>
Enter fullscreen mode Exit fullscreen mode

DONE!! Now if we write the post's description in markdown format, it will be able to render it.

Screenshot 2021-08-18 at 5.20.40 PM.png

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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Screenshot 2021-08-18 at 6.02.57 PM.png

Optional resources

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.

Top comments (0)