Full Steam Ahead: Getting Started with Ruby on Rails

Laying the Tracks for Web Development with Rails!

Before we start

If you’re here, you probably already know Ruby. If not, check out this blog post, which goes over how to get started with the language.

So, what is Ruby on Rails?

In technical terms, Rails is an MVC web server-side framework that prioritizes conventions over configurations.

Ok, that was a lot of buzzwords, let’s break it down.

MVC

MVC stands for Model-View-Controller, a design pattern used in software development to separate concerns and organize code in a structured way.

Model: Represents the data and business logic of the application. It interacts with the database and handles data-related operations.

View: Handles the presentation layer. It displays data to the user and manages the user interface.

Controller: Acts as the intermediary between the Model and the View. It processes user input, interacts with the Model, and updates the View accordingly.

Server-side framework

A server-side framework operates on the server, meaning it handles the backend logic of a web application. This includes processing requests from users, interacting with databases, and sending responses back to the client (e.g., a browser).

Convention over Configuration

Convention over Configuration is a design philosophy where the framework provides sensible defaults and standard ways of doing things, reducing the need for explicit configuration.

Think of it like buying a meal at a self-service restaurant versus ordering at a traditional restaurant:

With Convention over Configuration (self-service approach), you follow a predefined flow — grab a plate, choose from prepared foods arranged in a standard order, weigh, and pay. The system works efficiently because everyone follows the same conventions without having to specify every detail.

With Configuration-heavy frameworks (traditional restaurant), you need to explicitly order each item, specify how it’s prepared, and make many individual decisions about your meal.

Getting Started

Now that we have a good concept of what Rails is, we can play with it a bit to see its true potential.

Setup

I’m assuming that you already have ruby installed. For reference, I’ll be using ruby 3.4.2 and rails 8.0.2.

First, you need to install Rails, using your terminal of choice, type this:

gem install rails

With Rails installed, you can now create a new Rails project using the command:

rails new your-app-name

There are a lot of additional configs that you can pass with the rails new command that can help you save time. To see all available options, you can type rails new --help.

For now, let’s just use the basic setup. What you need to know is that the default database is sqlite3 and that this is a full-stack app.

After you successfully created your app, go to your apps folder, cd your-app-name and then run rails server or bin/dev to start your server and then go to localhost:3000.

And there you have it, you created your first Rails app, it doesn’t do anything (yet) but it’s something.

Files and Project Structure

There are a lot of files and folders, but the ones you should know for now are the following:

Directory/FileDescription
app/Contains the controllers, models, views, helpers, mailers, jobs, and assets for your application. We’ll focus mostly on this folder.
bin/Contains the Rails script that starts your app and other scripts for setup, update, deployment, etc.
config/Contains configuration for your application’s routes, database, and more.
db/Contains your current database schema and database migrations.
Gemfile/Gemfile.lockThese files specify gem dependencies needed for your Rails application. Used by the Bundler gem.
lib/Extended modules for your application.
storage/Contains SQLite databases and Active Storage files for Disk Service.
test/Unit tests, fixtures, and other test apparatus.
tmp/Temporary files (like cache and pid files).
.rubocop.ymlConfiguration for RuboCop.
.ruby-versionContains the default Ruby version.

If you want more details on the other files and folders, you can check the official docs.

Let’s take a quick look at the database configuration, go to config/database.yml.

default: &default
  adapter: sqlite3
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  timeout: 5000

development:
  <<: *default
  database: storage/development.sqlite3

Here you can add additional configuration to your database, like username, password, additional protection, remote address, etc. Since we’re using SQLite, there aren’t any additional configs that we need to add to get things moving.

Making things happen

Go to your console and run rails generate scaffold Post name:string title:string content:text.

You may notice that Rails created a lot of files; we’ll check’em out later. Try restarting your app and going back to localhost:3000.

You are going to see your first error (cheers!), it may look something like this:

Pending migrations error

Have no fear, the error just says that you have pending migrations, and it also tells you how to solve them, stop your server, and type bin/rails db:migrate to run your migrations.

Let’s take a quick detour to talk about what migrations are. Think of them like building instructions, but instead of building some furniture, you are building your database structure (creating tables, adding columns, etc.). Migrations should be reversible (which, if you are writing them with Ruby, they generally are), versioned, and database-agnostic.

If you go to db/migrate/ you should see a new file that looks like this:

class CreatePosts < ActiveRecord::Migration[8.0]
  def change
    create_table :posts do |t|
      t.string :name
      t.string :title
      t.text :content

      t.timestamps
    end
  end
end

This migration is saying to create a new table (posts), with a few columns: name, title, content and timestamps. The first three are self-evident, while the timestamps column is telling Rails to create 2 columns, created_at and updated_at.

Ok, with that out of the way, go to localhost:3000/posts, you should see something like this:

Empty index page

Before we start creating posts, we should talk about how we got here. How did Rails know what the url localhost:3000/posts was? When you ran the scaffold command, Rails created a few things for you. Let’s take a closer look at what those are.

What we created

scaffold is a powerful command that helps you create a complete set of MVC components for a resource (in our case: Post). When this is ran, Rails creates a few things:

  • Database migration – Which we already saw.
  • Model – A model class inheriting from ApplicationRecord
  • Controller – With all seven RESTful actions (index, show, new, create, edit, update, destroy)
  • Views – HTML templates for displaying, creating, and editing resources
  • Routes – The resources route configuration for all standard endpoints
  • Tests – Basic test files for all generated components

We will check them out with more detail in the following subsections.

Routes

Go to config/routes.rb, this file is where you will define all your application routes, for example /posts. Your file should look similar to this one:

Rails.application.routes.draw do
  resources :posts
  get "up" => "rails/health#show", as: :rails_health_check
end

Look at line where it says resources :posts. This method creates seven different routes for you:

indexGET/posts
newGET/posts/new
createPOST/posts
showGET/posts/:id
editGET/posts/:id/edit
updatePATCH/PUT/posts/:id
destroyDELETE/posts/:id

And maps them to the Posts controller. When you went to localhost:3000/posts you did a GET to the /posts route, which is mapped to your index method inside your controller.

Go to app/controllers/posts_controller.rb.

The only thing inside the index method is the line @posts = Post.all, this is querying all your Posts and storing them in the @posts instance variable. To understand this line, go to app/models/post.rb.

Model

class Post < ApplicationRecord
end

This is your model class for Posts, you can see that it inherits ApplicationRecord, which inherits ActiveRecord::Base, ActiveRecord objects don’t need to specify their attributes, instead, they infer them from the database table. Another neat thing is that ActiveRecord comes with a very rich query interface, that allows you to retrieve (like you saw happen inside the index with Post.all), create, update, and delete records without the need to write any SQL.

View

Continuing talking about the index method, Rails makes it so your @posts variable is available in the corresponding view file, open the file at app/views/posts/index.html.erb.

<p style="color: green"><%= notice %></p>

<% content_for :title, "Posts" %>

<h1>Posts</h1>

<div id="posts">
  <% @posts.each do |post| %>
    <%= render post %>
    <p>
      <%= link_to "Show this post", post %>
    </p>
  <% end %>
</div>

<%= link_to "New post", new_post_path %>

You can see that this file contains very basic HTML and some ruby code.

Thanks to naming conventions (and some magic), Rails understands that this HTML file is related to the index method of your Post controller.

Now, going back to your browser, in the localhost:3000/posts url, try clicking New Post, you should get directed to a page that looks like this:

New post empty

It’s a straightforward form, that contains every field needed to create a Post, if you check your url, you may notice that you were directed to /posts/new, which by convention, is a GET request to a web page that contains a form to create your entity.

If you go back to your controller and check the new method you may notice there is a single line @post = Post.new this will store an empty Post in your @post variable, that you may have already guessed, is available in the corresponding view under app/views/posts/new.html.erb.

<% content_for :title, "New post" %>

<h1>New post</h1>

<%= render "form", post: @post %>

<br>

<div>
  <%= link_to "Back to posts", posts_path %>
</div>

You may be wondering where the form actually is, if you check the line <%= render "form" %> you will see that this line renders a partial view called form, that is under app/views/posts/_form.html.erb.

Partials are marked with an underscore _ before their name.

<%= form_with(model: post) do |form| %>
  <% if post.errors.any? %>
    <div style="color: red">
      <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>
    <%= form.label :name, style: "display: block" %>
    <%= form.text_field :name %>
  </div>

  <div>
    <%= form.label :title, style: "display: block" %>
    <%= form.text_field :title %>
  </div>

  <div>
    <%= form.label :content, style: "display: block" %>
    <%= form.textarea :content %>
  </div>

  <div>
    <%= form.submit %>
  </div>
<% end %>

This HTML page uses Rails’ form helper to elegantly build a form. The form_with method uses your model to infer the needed url, since you passed an empty Post, it knows that the form should be empty, and that the target endpoint should be a POST instead of a PATCH or PUT.

Now go back to your browser and create a new post!

New post filled

When you hit create post, your form will be submitted to your POST /posts route, that is related to your create method in your controller. Let’s check it out.

Controller

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

A few things to unpack here, first of all, let’s take a look at the method that is referenced in the first line @post = Post.new(post_params), if you go to the end of your file, you will see 2 private methods, one of them being post_params.

def post_params
    params.expect(post: [ :name, :title, :content ])
end

The params method contains all parameters passed with the request, and the expect is saying which of them are “allowed” to pass through, in this case, you are expecting an object post with three keys, name, title and content.

Going back to the create method, the respond_to method is beign used to have different responses based on the client’s request, in this case, we can receive html and json, let’s focus on the html part since it is what we are doing.

You may notice that there is a conditional if @post.save, which checks whether saving the record was successful or not.

In our case, since there was nothing wrong with our request, we got the “happy path” format.html { redirect_to @post, notice: "Post was successfully created." }, this makes it so you get redirected to the created post page, that by convention is under posts/:id, that is linked to the show method.

And you guessed it, we are going to talk about it!

If you go to the show method, you will notice it is empty.

def show
end

It may seem odd at first, but if you look at the top of your controller you’ll see before_action :set_post, only: %i[ show edit update destroy ]. This means that before running those actions (show, edit, update, and destroy), Rails will call the private set_post method in your controller.

def set_post
    @post = Post.find(params.expect(:id))
end

This is using ActiveRecord’s powerful querying tools to find a Post with the id passed on the request and storing it in the @post instance variable.

Wrapping up

With this, you learned the basics of a Rails app, we saw all the layers mentioned in the beginning (Model, View, Controller), and how they seamlessly connect to create a functional web application.

Rails’ “convention over configuration” philosophy showed its power and potential. With minimal code, you’ve created a fully functional CRUD application for managing posts. The power of ActiveRecord allowed you to interact with your database without writing a single line of SQL, while Rails’ routing system automatically handled the mapping between URLs and controller actions.

Remember that what we’ve covered is just the tip of the iceberg. Stay tuned for future posts.

Previously: Setting the backstage: Rack and Sinatra

This post is part of our ‘The Miners’ Guide to Code Crafting’ series, designed to help aspiring developers learn and grow. Stay tuned for more and continue your coding journey with us!! Check out the full summary here!

We want to work with you. Check out our "What We Do" section!