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/File | Description |
---|---|
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.lock | These 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.yml | Configuration for RuboCop. |
.ruby-version | Contains 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:
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:
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:
index | GET | /posts |
new | GET | /posts/new |
create | POST | /posts |
show | GET | /posts/:id |
edit | GET | /posts/:id/edit |
update | PATCH/PUT | /posts/:id |
destroy | DELETE | /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:
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!
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!