Ruby on Rails is the web framework that made web development fast and enjoyable. Twitter, GitHub, Shopify, and Airbnb all started on Rails. If you want to build real web applications quickly without boilerplate, Rails is the right choice. This tutorial takes you from zero to a working Rails application — no prior Rails experience required.
What you'll learn
| Topic | What you'll be able to do |
|---|---|
| Setup | Install Ruby and Rails, create your first app |
| MVC | Understand Models, Views, Controllers |
| ActiveRecord | Query and manage a database with zero SQL |
| Routes | Map URLs to controller actions |
| Views | Build dynamic HTML with ERB templates |
| Forms | Create and validate user input |
| Authentication | Add login/signup with Devise |
| REST API | Build a JSON API |
| Testing | Write RSpec unit and request specs |
| Deploy | Ship to Fly.io or Render |
Why Ruby on Rails?
| Rails | Django | Laravel | Express | |
|---|---|---|---|---|
| Language | Ruby | Python | PHP | JavaScript |
| Philosophy | Convention over Configuration | Batteries included | Elegant syntax | Minimalist |
| Learning curve | Low-Medium | Medium | Medium | Low |
| Built-in ORM | ActiveRecord | Django ORM | Eloquent | None |
| Database migrations | Yes | Yes | Yes | Manual |
| Admin panel | ActiveAdmin (gem) | Built-in | Nova (paid) | None |
| Best for | Full-stack CRUD apps, startups | Data-heavy apps | PHP teams | Microservices |
| Job market | ~80k+ jobs | ~120k+ jobs | ~100k+ jobs | ~300k+ jobs |
What Rails is great for
- CRUD web applications (blogs, SaaS, marketplaces)
- Rapid prototyping — idea to working app in hours
- Admin dashboards and internal tools
- REST APIs and JSON backends
- Startups (speed of development is a superpower)
1. Installation and Setup
Install Ruby
macOS (recommended: rbenv):
brew install rbenv ruby-build
rbenv install 3.3.0
rbenv global 3.3.0
ruby --version # ruby 3.3.0
Ubuntu/Debian:
sudo apt install rbenv
rbenv install 3.3.0
rbenv global 3.3.0
Windows: Use WSL2 + Ubuntu, then follow the Linux steps.
Install Rails
gem install rails
rails --version # Rails 7.2.x (or similar)
Create your first Rails app
rails new myapp --database=postgresql
cd myapp
What rails new generates:
| Directory/File | Purpose |
|---|---|
app/ |
Your application code (MVC) |
app/models/ |
ActiveRecord models |
app/controllers/ |
Controllers |
app/views/ |
ERB templates |
config/routes.rb |
URL routing |
db/ |
Database schema and migrations |
Gemfile |
Ruby gem dependencies |
test/ or spec/ |
Tests |
Start the server
bin/rails db:create
bin/rails server # or: rails s
Open http://localhost:3000 — you'll see the Rails welcome page.
2. MVC Architecture
Rails is built on the Model-View-Controller pattern.
Browser → Routes → Controller → Model → Database
↓
View → HTML → Browser
| Layer | File location | Responsibility |
|---|---|---|
| Model | app/models/ |
Business logic, database queries |
| View | app/views/ |
HTML templates (ERB) |
| Controller | app/controllers/ |
Handle requests, coordinate model+view |
| Routes | config/routes.rb |
Map URLs to controller actions |
Request lifecycle
- Browser sends
GET /posts - Router matches route →
PostsController#index - Controller calls
Post.all(Model) - Model queries database, returns records
- Controller passes data to view (
@posts) - View renders HTML with
@posts - Controller sends HTML back to browser
3. Your First Resource: Posts
Generate a scaffold
Rails can generate a complete CRUD resource in one command:
bin/rails generate scaffold Post title:string body:text published:boolean
This generates:
- Model (
app/models/post.rb) - Controller (
app/controllers/posts_controller.rb) - Views (
app/views/posts/) - Migration (
db/migrate/xxx_create_posts.rb) - Routes (added to
config/routes.rb)
Run the migration
bin/rails db:migrate
Now visit http://localhost:3000/posts — full CRUD app, no code written.
What the scaffold created
Controller (app/controllers/posts_controller.rb):
class PostsController < ApplicationController
before_action :set_post, only: %i[show edit update destroy]
def index
@posts = Post.all
end
def show
end
def new
@post = Post.new
end
def create
@post = Post.new(post_params)
if @post.save
redirect_to @post, notice: "Post was successfully created."
else
render :new, status: :unprocessable_entity
end
end
def update
if @post.update(post_params)
redirect_to @post, notice: "Post was successfully updated."
else
render :edit, status: :unprocessable_entity
end
end
def destroy
@post.destroy
redirect_to posts_url, notice: "Post was successfully destroyed."
end
private
def set_post
@post = Post.find(params[:id])
end
def post_params
params.require(:post).permit(:title, :body, :published)
end
end
Index view (app/views/posts/index.html.erb):
<h1>Posts</h1>
<% @posts.each do |post| %>
<div>
<h2><%= post.title %></h2>
<p><%= post.body %></p>
<%= link_to "Show", post %>
<%= link_to "Edit", edit_post_path(post) %>
<%= link_to "Delete", post, data: { turbo_method: :delete, turbo_confirm: "Are you sure?" } %>
</div>
<% end %>
<%= link_to "New Post", new_post_path %>
4. Routes
Routes map URLs and HTTP methods to controller actions.
config/routes.rb:
Rails.application.routes.draw do
resources :posts # generates 7 RESTful routes
root "posts#index" # homepage
end
RESTful routes table
| HTTP Method | URL | Controller action | Purpose |
|---|---|---|---|
| GET | /posts |
index |
List all posts |
| GET | /posts/new |
new |
Form to create |
| POST | /posts |
create |
Save new post |
| GET | /posts/:id |
show |
Show one post |
| GET | /posts/:id/edit |
edit |
Form to edit |
| PATCH/PUT | /posts/:id |
update |
Save edits |
| DELETE | /posts/:id |
destroy |
Delete post |
View routes
bin/rails routes
Named route helpers
posts_path # => "/posts"
new_post_path # => "/posts/new"
post_path(@post) # => "/posts/1"
edit_post_path(@post) # => "/posts/1/edit"
Nested routes
resources :posts do
resources :comments, only: [:create, :destroy]
end
# generates: /posts/:post_id/comments
5. ActiveRecord — Database Without SQL
ActiveRecord is Rails' ORM. It maps Ruby classes to database tables.
Creating a model
bin/rails generate model Comment post:references body:text author:string
bin/rails db:migrate
CRUD operations
# CREATE
post = Post.create(title: "Hello Rails", body: "My first post", published: true)
# or:
post = Post.new(title: "Hello")
post.save
# READ
Post.all # all records
Post.find(1) # find by id (raises error if not found)
Post.find_by(title: "Hello") # find by attribute (returns nil if not found)
Post.where(published: true) # returns array-like relation
Post.order(created_at: :desc) # ordering
Post.limit(10).offset(20) # pagination
# UPDATE
post.update(title: "Updated Title")
# or:
post.title = "New Title"
post.save
# DELETE
post.destroy
Post.where(published: false).destroy_all
Querying
# Conditions
Post.where("created_at > ?", 1.week.ago)
Post.where(published: true).order(:created_at)
# Counting
Post.count
Post.where(published: true).count
# Aggregates
Post.average(:views)
Post.sum(:views)
Post.maximum(:created_at)
# Joining
Post.joins(:comments).where(comments: { author: "Alice" })
# Eager loading (avoids N+1 queries)
Post.includes(:comments).all
Scopes
class Post < ApplicationRecord
scope :published, -> { where(published: true) }
scope :recent, -> { order(created_at: :desc).limit(10) }
scope :by_author, ->(name) { where(author: name) }
end
Post.published.recent
Post.by_author("Alice").published
Associations
class Post < ApplicationRecord
belongs_to :user
has_many :comments, dependent: :destroy
has_many :tags, through: :post_tags
has_one :featured_image, dependent: :destroy
end
class Comment < ApplicationRecord
belongs_to :post
belongs_to :user
end
# Usage
post.comments # all comments for this post
post.comments.create(body: "Great post!")
comment.post # the post this comment belongs to
Association types
| Association | Method | Example |
|---|---|---|
| One-to-Many | has_many / belongs_to |
Post has_many Comments |
| One-to-One | has_one / belongs_to |
User has_one Profile |
| Many-to-Many | has_many :through |
Post has_many Tags through PostTags |
| Many-to-Many (simple) | has_and_belongs_to_many |
Post has_and_belongs_to_many Categories |
6. Validations
Validations run before saving to the database.
class Post < ApplicationRecord
validates :title, presence: true, length: { minimum: 5, maximum: 100 }
validates :body, presence: true
validates :slug, uniqueness: true, format: { with: /\A[a-z0-9-]+\z/ }
validates :status, inclusion: { in: %w[draft published archived] }
validate :title_must_not_contain_profanity
private
def title_must_not_contain_profanity
if title&.include?("badword")
errors.add(:title, "must not contain inappropriate language")
end
end
end
Common validators
| Validator | Example |
|---|---|
presence |
validates :name, presence: true |
uniqueness |
validates :email, uniqueness: true |
length |
validates :bio, length: { maximum: 500 } |
numericality |
validates :age, numericality: { greater_than: 0 } |
format |
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP } |
inclusion |
validates :role, inclusion: { in: %w[admin user guest] } |
confirmation |
validates :password, confirmation: true |
Checking validity
post = Post.new(title: "Hi") # too short
post.valid? # => false
post.errors.full_messages # => ["Title is too short (minimum is 5 characters)"]
post.save # => false (does not save)
7. Views and ERB Templates
ERB (Embedded Ruby) mixes Ruby into HTML.
<% %> <!-- Execute Ruby, no output -->
<%= %> <!-- Execute Ruby AND output result -->
<%# %> <!-- Comment (not rendered) -->
Example view (app/views/posts/show.html.erb):
<article>
<h1><%= @post.title %></h1>
<p class="meta">
Published <%= time_ago_in_words(@post.created_at) %> ago
<% if @post.published? %>
<span class="badge">Published</span>
<% else %>
<span class="badge draft">Draft</span>
<% end %>
</p>
<div class="body">
<%= simple_format(@post.body) %>
</div>
<h2>Comments (<%= @post.comments.count %>)</h2>
<% @post.comments.each do |comment| %>
<div class="comment">
<strong><%= comment.author %></strong>
<p><%= comment.body %></p>
</div>
<% end %>
</article>
<%= link_to "Edit", edit_post_path(@post) %>
<%= link_to "Back", posts_path %>
Layouts
The application layout wraps all views:
app/views/layouts/application.html.erb:
<!DOCTYPE html>
<html>
<head>
<title>MyApp</title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag "application", data: { turbo_track: "reload" } %>
<%= javascript_importmap_tags %>
</head>
<body>
<nav>
<%= link_to "Home", root_path %>
<%= link_to "Posts", posts_path %>
</nav>
<% if notice %>
<div class="notice"><%= notice %></div>
<% end %>
<main>
<%= yield %> <!-- view content goes here -->
</main>
</body>
</html>
Partials
Reusable view fragments start with _:
<!-- app/views/posts/_post.html.erb -->
<div class="post">
<h2><%= post.title %></h2>
<p><%= truncate(post.body, length: 150) %></p>
<%= link_to "Read more", post %>
</div>
Render in a view:
<!-- renders _post.html.erb for each post -->
<%= render @posts %>
<!-- or explicitly: -->
<%= render partial: "post", locals: { post: @post } %>
View helpers
link_to "Click me", posts_path # <a href="/posts">Click me</a>
image_tag "logo.png", alt: "Logo" # <img src="..." alt="Logo">
time_ago_in_words(post.created_at) # "3 minutes"
number_to_currency(49.99) # "$49.99"
truncate("Long text...", length: 50) # "Long tex..."
simple_format("Line 1\nLine 2") # wraps in <p> tags
pluralize(post.comments.count, "comment") # "3 comments"
8. Forms
Rails forms use form_with helper.
New post form (app/views/posts/new.html.erb):
<h1>New Post</h1>
<%= form_with model: @post do |form| %>
<% if @post.errors.any? %>
<div class="errors">
<h2><%= pluralize(@post.errors.count, "error") %> prevented saving:</h2>
<ul>
<% @post.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>
<div>
<%= form.label :title %>
<%= form.text_field :title, placeholder: "Enter title..." %>
</div>
<div>
<%= form.label :body %>
<%= form.text_area :body, rows: 10 %>
</div>
<div>
<%= form.label :published %>
<%= form.check_box :published %>
</div>
<%= form.submit "Save Post" %>
<% end %>
<%= link_to "Cancel", posts_path %>
Form field helpers
| Helper | HTML output |
|---|---|
form.text_field :name |
<input type="text"> |
form.text_area :body |
<textarea> |
form.check_box :active |
<input type="checkbox"> |
form.select :role, ["admin","user"] |
<select> |
form.email_field :email |
<input type="email"> |
form.password_field :password |
<input type="password"> |
form.number_field :age |
<input type="number"> |
form.file_field :avatar |
<input type="file"> |
form.hidden_field :token |
<input type="hidden"> |
form.submit "Save" |
<input type="submit"> |
9. Authentication with Devise
Devise is the standard Rails authentication gem.
Install
# Gemfile
gem "devise"
bundle install
bin/rails generate devise:install
bin/rails generate devise User
bin/rails db:migrate
What Devise adds
Usermodel with email/password/confirmable/recoverable- Routes: sign_in, sign_up, sign_out, password reset, confirmation
- Controllers and views for all auth flows
current_userhelper in controllers and viewsauthenticate_user!before action
Protect routes
class PostsController < ApplicationController
before_action :authenticate_user!, except: [:index, :show]
# now only logged-in users can create/edit/delete
end
In views
<% if user_signed_in? %>
Logged in as <%= current_user.email %>
<%= link_to "Sign out", destroy_user_session_path, data: { turbo_method: :delete } %>
<% else %>
<%= link_to "Sign in", new_user_session_path %>
<%= link_to "Sign up", new_user_registration_path %>
<% end %>
Add user to posts
bin/rails generate migration AddUserToPosts user:references
bin/rails db:migrate
class Post < ApplicationRecord
belongs_to :user
end
class User < ApplicationRecord
has_many :posts, dependent: :destroy
end
# PostsController
def create
@post = current_user.posts.build(post_params)
# ...
end
10. Building a REST API
Rails can serve JSON APIs without any views.
API-only app
rails new myapi --api --database=postgresql
JSON responses
class Api::V1::PostsController < ApplicationController
def index
posts = Post.published.order(created_at: :desc)
render json: posts, status: :ok
end
def show
post = Post.find(params[:id])
render json: post, status: :ok
rescue ActiveRecord::RecordNotFound
render json: { error: "Post not found" }, status: :not_found
end
def create
post = Post.new(post_params)
if post.save
render json: post, status: :created
else
render json: { errors: post.errors.full_messages }, status: :unprocessable_entity
end
end
private
def post_params
params.require(:post).permit(:title, :body, :published)
end
end
API routes
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
resources :posts, only: [:index, :show, :create, :update, :destroy]
end
end
end
Custom serialization with jbuilder
# app/views/api/v1/posts/show.json.jbuilder
json.id @post.id
json.title @post.title
json.body @post.body
json.published_at @post.created_at.iso8601
json.author do
json.id @post.user.id
json.name @post.user.name
end
json.comments @post.comments do |comment|
json.id comment.id
json.body comment.body
json.author comment.user.name
end
JWT authentication for APIs
# Gemfile
gem "jwt"
# Generate token
def encode_token(payload)
JWT.encode(payload, Rails.application.secret_key_base, "HS256")
end
# Decode token
def decode_token(token)
JWT.decode(token, Rails.application.secret_key_base, true, algorithm: "HS256")
rescue JWT::DecodeError
nil
end
11. Background Jobs with Sidekiq
For long-running tasks (emails, image processing, data import).
# Gemfile
gem "sidekiq"
# app/jobs/notification_job.rb
class NotificationJob < ApplicationJob
queue_as :default
def perform(user_id, message)
user = User.find(user_id)
UserMailer.notification(user, message).deliver_now
end
end
# Enqueue from anywhere
NotificationJob.perform_later(current_user.id, "Your post was published!")
NotificationJob.set(wait: 5.minutes).perform_later(user.id, "Reminder!")
Start Sidekiq:
bundle exec sidekiq
12. Testing with RSpec
# Gemfile (development, test group)
group :development, :test do
gem "rspec-rails"
gem "factory_bot_rails"
gem "faker"
end
bin/rails generate rspec:install
Model spec
# spec/models/post_spec.rb
require "rails_helper"
RSpec.describe Post, type: :model do
describe "validations" do
it "is valid with valid attributes" do
post = build(:post)
expect(post).to be_valid
end
it "requires a title" do
post = build(:post, title: nil)
expect(post).not_to be_valid
expect(post.errors[:title]).to include("can't be blank")
end
it "requires title to be at least 5 characters" do
post = build(:post, title: "Hi")
expect(post).not_to be_valid
end
end
describe ".published" do
it "returns only published posts" do
published = create(:post, published: true)
draft = create(:post, published: false)
expect(Post.published).to include(published)
expect(Post.published).not_to include(draft)
end
end
end
Factory
# spec/factories/posts.rb
FactoryBot.define do
factory :post do
title { Faker::Lorem.sentence(word_count: 5) }
body { Faker::Lorem.paragraphs(number: 3).join("\n") }
published { true }
association :user
end
end
Request spec
# spec/requests/posts_spec.rb
require "rails_helper"
RSpec.describe "Posts API", type: :request do
let(:user) { create(:user) }
let(:headers) { { "Authorization" => "Bearer #{auth_token(user)}" } }
describe "GET /api/v1/posts" do
before { create_list(:post, 3, user: user) }
it "returns all posts" do
get "/api/v1/posts", headers: headers
expect(response).to have_http_status(:ok)
expect(JSON.parse(response.body).length).to eq(3)
end
end
describe "POST /api/v1/posts" do
let(:valid_params) { { post: { title: "New Post Title", body: "Body content here", published: true } } }
it "creates a post" do
expect {
post "/api/v1/posts", params: valid_params, headers: headers
}.to change(Post, :count).by(1)
expect(response).to have_http_status(:created)
end
it "returns errors for invalid params" do
post "/api/v1/posts", params: { post: { title: "" } }, headers: headers
expect(response).to have_http_status(:unprocessable_entity)
end
end
end
13. Migrations
Migrations version-control your database schema.
# Generate migrations
bin/rails generate migration AddSlugToPosts slug:string:uniq
bin/rails generate migration AddIndexToPostsTitle
bin/rails generate migration RemovePublishedFromPosts published:boolean
Example migration:
class AddSlugAndIndexToPosts < ActiveRecord::Migration[7.2]
def change
add_column :posts, :slug, :string
add_column :posts, :views_count, :integer, default: 0, null: false
add_index :posts, :slug, unique: true
add_index :posts, :published
add_index :posts, [:user_id, :created_at]
end
end
Migration commands
| Command | What it does |
|---|---|
rails db:migrate |
Run pending migrations |
rails db:rollback |
Undo last migration |
rails db:rollback STEP=3 |
Undo last 3 migrations |
rails db:migrate:status |
Show migration status |
rails db:schema:load |
Load schema fresh |
rails db:seed |
Run db/seeds.rb |
rails db:reset |
Drop + create + migrate + seed |
14. Performance Tips
Avoid N+1 queries
# BAD — N+1 (1 query for posts + 1 per post for user)
@posts = Post.all
# In view: post.user.name # runs query for EACH post
# GOOD — eager load
@posts = Post.includes(:user).all
# In view: post.user.name # no extra queries
# Also: includes with conditions
@posts = Post.includes(:comments).where(published: true)
Detect N+1 with Bullet gem
# Gemfile (development)
gem "bullet"
Pagination
# Gemfile
gem "pagy"
# Controller
include Pagy::Backend
@pagy, @posts = pagy(Post.published.order(created_at: :desc), items: 20)
# View
include Pagy::Frontend
<%= pagy_nav(@pagy) %>
Caching
# Cache a fragment
<% cache @post do %>
<%= render @post %>
<% end %>
# Cache a value
Rails.cache.fetch("popular_posts", expires_in: 1.hour) do
Post.published.order(views_count: :desc).limit(10).to_a
end
15. Deploy to Fly.io
# Install flyctl
curl -L https://fly.io/install.sh | sh
# Login
fly auth login
# Launch app
fly launch
# Set environment variables
fly secrets set RAILS_MASTER_KEY=$(cat config/master.key)
# Deploy
fly deploy
# Run migrations
fly ssh console -C "bin/rails db:migrate"
fly.toml (auto-generated, key sections):
[build]
[env]
PORT = "8080"
RAILS_ENV = "production"
[http_service]
internal_port = 8080
force_https = true
16. Build This: Blog App
Putting it all together — a blog with users, posts, and comments.
# Create app
rails new blog --database=postgresql
cd blog
# Add gems
bundle add devise pagy rspec-rails factory_bot_rails
# Generate models
bin/rails generate devise:install
bin/rails generate devise User name:string
bin/rails generate scaffold Post title:string body:text slug:string published:boolean user:references
bin/rails generate model Comment body:text user:references post:references
bin/rails db:migrate
# Add to routes
# resources :posts do
# resources :comments, only: [:create, :destroy]
# end
# root "posts#index"
Seed data (db/seeds.rb):
user = User.create!(
name: "Alice",
email: "alice@example.com",
password: "password123"
)
5.times do |i|
Post.create!(
title: "Post #{i + 1}: Getting Started with Rails",
body: "Rails is a web application framework...",
published: true,
user: user
)
end
puts "Created #{User.count} users and #{Post.count} posts"
bin/rails db:seed
bin/rails server
Common Mistakes
| Mistake | Problem | Fix |
|---|---|---|
| N+1 queries | Slow pages | Use includes() to eager load |
No strong_params |
Mass assignment vulnerability | Always use params.permit() |
| Logic in views | Unmaintainable code | Move to models or helpers |
| Fat controllers | Hard to test | Move business logic to models/services |
| Missing indexes | Slow queries | Index foreign keys and frequently queried columns |
| Storing secrets in code | Security breach | Use credentials.yml.enc or ENV vars |
Skipping validations with save! |
Uncaught errors | Handle ActiveRecord::RecordInvalid |
Forgetting dependent: :destroy |
Orphaned records | Add to has_many associations |
Rails vs related technologies
| Purpose | When to use | |
|---|---|---|
| Rails | Full-stack web framework | CRUD apps, SaaS, admin panels |
| Sinatra | Minimal Ruby framework | Simple APIs, microservices |
| Hanami | Modern Ruby framework | Clean architecture, performance |
| Devise | Authentication gem | Login/signup in any Rails app |
| ActiveAdmin | Admin panel gem | Internal dashboards fast |
| Sidekiq | Background jobs | Async tasks, email, processing |
| Hotwire (Turbo+Stimulus) | SPA-like UX without JS framework | Dynamic Rails pages |
| Pagy | Pagination gem | Paginating large collections |
| RSpec | Testing framework | Unit, integration, request specs |
| Fly.io / Render | Hosting platforms | Deploy Rails apps easily |
Ruby on Rails developer roles and salary
| Role | Experience | Salary (US) |
|---|---|---|
| Junior Rails Developer | 0–2 years | $65k–$90k |
| Mid-level Rails Developer | 2–5 years | $90k–$130k |
| Senior Rails Developer | 5+ years | $130k–$175k |
| Rails Tech Lead | 7+ years | $150k–$200k |
| Fullstack (Rails + React) | 3+ years | $110k–$160k |
Learning path
| Stage | Topics | Time |
|---|---|---|
| 1. Ruby basics | Variables, methods, classes, blocks, modules | 2–3 weeks |
| 2. Rails fundamentals | MVC, routes, CRUD, ActiveRecord | 3–4 weeks |
| 3. Authentication | Devise, sessions, authorization (Pundit/CanCanCan) | 1–2 weeks |
| 4. Testing | RSpec, FactoryBot, Capybara | 2 weeks |
| 5. Frontend | Hotwire Turbo+Stimulus, or React/Vue with Rails API | 2–4 weeks |
| 6. Background jobs | Sidekiq, Action Mailer, Active Storage | 1–2 weeks |
| 7. Deploy & ops | Fly.io/Render, environment config, caching | 1 week |
| 8. Portfolio | Build 2–3 real apps, push to GitHub | Ongoing |
FAQ
Q: Do I need to learn Ruby before Rails? A: Yes — learn Ruby basics first (1–2 weeks). You don't need to be an expert, but you need to understand classes, blocks, and modules. Try Ruby in 20 Minutes is a good starting point.
Q: Is Rails still relevant in 2025? A: Yes. Rails powers major companies (GitHub, Shopify, Basecamp, Airbnb). Rails 7 added Hotwire for reactive UIs without heavy JavaScript. The ecosystem is active and thriving.
Q: Rails vs Django — which should I learn? A: If you know Python or plan to work in data science, choose Django. If you prefer Ruby's expressiveness and want the fastest path to building full-stack apps, choose Rails. Both are excellent choices.
Q: How long to get a Rails job? A: With dedicated study (8–12 hours/week), most people are job-ready in 4–6 months. Build real projects, contribute to open-source Rails apps, and deploy something publicly.
Q: What's Hotwire? A: Hotwire (Turbo + Stimulus) ships with Rails 7 and gives you SPA-like interactivity — partial page updates, live search, real-time feeds — without writing a JavaScript framework. It's Rails' answer to React for most use cases.
Q: PostgreSQL vs MySQL for Rails?
A: PostgreSQL is recommended for Rails apps. It supports more advanced features (JSONB, full-text search, array columns) and is the default on most Rails hosting platforms. Use --database=postgresql when creating your app.