A practical, copy-ready guide to core Ruby, Enumerable patterns, OOP, Sinatra (+ ERB), testing, and a few advanced peeks. Built around the same patterns you’ve likely used: inventories, small APIs, and simple routes/views.
Table of contents
- Why this guide
- 1) Ruby fundamentals
- 2) Enumerable patterns (the 80/20)
- 3) OOP & modules in Ruby
- 4) Sinatra crash course (routes, params, views, helpers)
- 5) Mini testing intro (Minitest + Rack::Test)
- 6) Advanced peeks (procs, lambdas, meta, refinements, lazy)
- 7) Deployment notes (Render/Vercel-style providers)
- 8) Practice checklist
- 9) Tiny reference (copy/paste)
Why this guide
You don’t need a textbook to ship tiny apps. You need a tight toolkit:
- Ruby basics (data types, blocks, keyword args)
-
Enumerable muscle memory (
map
,select
,sum
,group_by
,tally
) -
OOP without overthinking (composition > inheritance,
attr_reader
) - Sinatra + ERB to wire routes → views, with a helper or two
- A whiff of testing and deployment so you can share it
Let’s go.
1) Ruby fundamentals
Values & literals
num = 42 # Integer
price = 29.99 # Float
ok = true # Boolean
name = "Ruby" # String
sym = :status # Symbol (great as Hash keys)
list = [1, 2, 3] # Array
info = {name: "Ada", lang: "Ruby"} # Hash (keyword style)
range = 1..3 # 1,2,3 (inclusive)
Truthiness: nil
and false
are falsey; everything else is truthy.
Strings & symbols
greeting = "Hi, #{name}" # interpolation
:ready == :ready # symbols are interned
Methods, default & keyword args
def greet(name = "world", excited: false)
msg = "Hello, #{name}"
excited ? "#{msg}!" : msg
end
greet # "Hello, world"
greet("Ada") # "Hello, Ada"
greet("Ada", excited: true) # "Hello, Ada!"
Multiple assignment & splats
a, b = [10, 20] # a=10, b=20
head, *rest = [1,2,3,4] # head=1, rest=[2,3,4]
Blocks & yield
def with_timer
t0 = Time.now
result = yield
puts "Took: #{Time.now - t0}s"
result
end
with_timer { sleep 0.1; 123 } # prints time, returns 123
2) Enumerable patterns (the 80/20)
Most collections include Enumerable
. These are the workhorses:
prices = [20, 50, 29.99, 65]
cheap = prices.select { |p| p < 30 } # filter in
not_cheap = prices.reject { |p| p < 30 } # filter out
doubled = prices.map { |p| p * 2 } # transform
total = prices.sum # aggregate
min_price = prices.min # 20
two_groups = prices.partition { |p| p < 30 } # [[<30], [>=30]]
For arrays of objects (e.g., boutique items):
items = [
{name: "Skirt", price: 29.99, quantity_by_size: {s: 1, xl: 4}},
{name: "Coat", price: 65.00, quantity_by_size: {}}
]
names = items.map { |i| i[:name] }.sort
out_of_stock= items.select { |i| i[:quantity_by_size].empty? }
total_stock = items.sum { |i| i[:quantity_by_size].values.sum }
Hash helpers you’ll use constantly:
h.fetch(:key) # raises if missing (great for required data)
h.fetch(:key, "N/A") # default
h.dig(:a, :b, :c) # safe deep lookup
{a:1,b:2}.transform_values { |v| v+1 } # => {a:2,b:3}
"rock paper rock".split.tally # => {"rock"=>2,"paper"=>1}
3) OOP & modules in Ruby
Minimal class, accessors, class vs instance methods
class Product
attr_reader :name, :price
attr_accessor :tags
def initialize(name:, price:, tags: [])
@name, @price, @tags = name, price, tags
end
def display
"#{name} — $#{price}"
end
def self.from_hash(h) # class method
new(**h)
end
end
p = Product.new(name: "Socks", price: 20)
p.display # "Socks — $20"
Product.from_hash(name: "Skirt", price: 29.99)
Modules as mixins (behavior), namespaces (organization)
module Discountable
def discounted(price: @price, by: 0.1)
(price * (1 - by)).round(2)
end
end
class CartItem
include Discountable
def initialize(price) ; @price = price ; end
end
CartItem.new(100).discounted(by: 0.2) # 80.0
Struct
and OpenStruct
Order = Struct.new(:id, :total, keyword_init: true)
o = Order.new(id: 1, total: 29.99)
require "ostruct"
row = { name: "Skirt", price: 29.99, quantity_by_size: {s:1, xl:4} }
item = OpenStruct.new(row)
item.name # "Skirt"
item.quantity_by_size.values.sum # 5
Rule of thumb: Struct
for known fields (faster); OpenStruct
when the shape varies (convenient).
4) Sinatra crash course (routes, params, views, helpers)
Minimal project layout
.
├─ app.rb
├─ views/
│ ├─ layout.erb
│ ├─ index.erb
│ └─ show.erb
└─ Gemfile
Gemfile
source "https://rubygems.org"
gem "sinatra"
gem "sinatra-contrib" # for reloader
gem "http" # simple HTTP client
gem "dotenv" # env vars for API keys (optional)
bundle install
Routes & params
# app.rb
require "sinatra"
require "sinatra/reloader" if development?
require "http"
require "json"
helpers do
def currencies
raw = HTTP.get("https://api.exchangerate.host/list").to_s
JSON.parse(raw).fetch("currencies") # => {"USD"=>"United States Dollar", ...}
end
end
get "/" do
@codes = currencies.keys.sort
erb :index
end
get "/:from" do
@from = params[:from].upcase
@codes = currencies.keys.sort
erb :show
end
get "/:from/:to" do
@from, @to = params.values_at("from", "to").map!(&:upcase)
url = "https://api.exchangerate.host/convert?from=#{@from}&to=#{@to}&amount=1"
data = JSON.parse(HTTP.get(url).to_s)
@rate = data["result"] || data.dig("info", "rate") || data.dig("info", "quote")
erb :convert
end
Views (ERB)
<!-- views/layout.erb -->
<!doctype html>
<html>
<head><meta charset="utf-8"><title>Exchange</title></head>
<body><main class="container"><%= yield %></main></body>
</html>
<!-- views/index.erb -->
<h1>Currency pairs</h1>
<ul>
<% @codes.each do |code| %>
<li><a href="/<%= code %>">Convert 1 <%= code %> to...</a></li>
<% end %>
</ul>
<!-- views/show.erb -->
<h1>Convert <%= @from %></h1>
<p>Go <a href="/">back</a>.</p>
<ul>
<% @codes.each do |to| %>
<li><a href="/<%= @from %>/<%= to %>">Convert 1 <%= @from %> to <%= to %>...</a></li>
<% end %>
</ul>
<!-- views/convert.erb -->
<h1>Convert <%= @from %> to <%= @to %></h1>
<p>1 <%= @from %> equals <%= @rate %> <%= @to %>.</p>
<p>Go <a href="/<%= @from %>">back</a>.</p>
ASCII request flow
Browser ──▶ Sinatra Route ──▶ (optional) Helper/Service ──▶ HTTP API
│ │ │ │
└─────────────┴────────── render(ERB) ◀────┴──── JSON ◀────┘
Tips
- Put rendering data into instance vars (
@foo
) inside routes. - Extract repeated logic into
helpers
or POROs (plain Ruby objects). - Use
params
for both path (/:id
) and query (?q=term
) parameters.
5) Mini testing intro (Minitest + Rack::Test)
# test_app.rb
require "minitest/autorun"
require "rack/test"
require_relative "./app"
class AppTest < Minitest::Test
include Rack::Test::Methods
def app = Sinatra::Application
def test_home_has_heading
get "/"
assert last_response.ok?
assert_includes last_response.body, "Currency pairs"
end
end
Run:
ruby test_app.rb
6) Advanced peeks (procs, lambdas, meta, refinements, lazy)
Procs vs lambdas
adder = ->(x, y) { x + y } # lambda (strict arity, normal return)
adder.call(2, 3) # 5
def with_proc
p = Proc.new { return :from_proc } # returns from method!
p.call
:after
end
with_proc # => :from_proc
Metaprogramming (define readers on the fly)
class Settings
def self.option(*names)
names.each { |n| define_method(n) { @data[n] } }
end
option :host, :port
def initialize(data) ; @data = data ; end
end
s = Settings.new(host: "localhost", port: 3000)
s.host # "localhost"
Refinements (scoped monkey-patch)
module Titleize
refine String do
def titleize = split.map!(&:capitalize).join(" ")
end
end
using Titleize
"hello world".titleize # "Hello World"
Lazy enumerators (streaming style)
(1..Float::INFINITY).lazy.select(&:odd?).map { |n| n*n }.first(5)
# => [1, 9, 25, 49, 81]
7) Deployment notes (Render/Vercel-style providers)
- Ensure your app binds to 0.0.0.0 and respects
PORT
. - Add a
Procfile
:
web: bundle exec ruby app.rb -p $PORT -o 0.0.0.0
- If using environment variables: add them in the provider’s dashboard (e.g.,
EXCHANGE_RATE_KEY
) and useENV.fetch(...)
in Ruby. - On free tiers, outbound HTTP may be rate-limited; for demos, consider caching JSON to a file during development.
8) Practice checklist
-
[ ] Refactor a hash-based inventory to
OpenStruct
and implement:-
item_names
(sorted) -
cheap
(price < 30) -
total_stock
(sum of nested values)
-
-
[ ] Build
/FROM
and/FROM/TO
Sinatra routes that:- render
<h1>
headings exactly as:Convert FROM
andConvert FROM to TO
- have “back” links to
/
and/FROM
respectively
- render
[ ] Add a helper for API calls and unit test one route with Rack::Test.
[ ] Replace a nested loop with
flat_map
.[ ] Write one
define_method
example (metaprogramming) just for fun.
9) Tiny reference (copy/paste)
# Enumerable
arr.select { |x| cond } ; arr.reject { |x| cond } ; arr.partition { |x| cond }
arr.map { |x| f(x) } ; arr.flat_map { |x| many(x) }
arr.sum ; arr.count { |x| cond } ; arr.min_by { |x| key }
arr.group_by { |x| key } ; arr.tally
h.fetch(:k, default) ; h.dig(:a,:b,:c) ; h.transform_values { |v| g(v) }
# OOP
class C
CONST = 1
attr_reader :a ; attr_accessor :b
def initialize(a:, b: 0) ; @a, @b = a, b ; end
def self.build ; new(a: 1) ; end
end
# Sinatra skeleton
require "sinatra" ; require "http" ; require "json"
get("/") { @data = JSON.parse(HTTP.get(URL).to_s) ; erb :index }
Final words
If you master: (1) the core Ruby syntax, (2) half a dozen Enumerable
moves, (3) small classes/modules, and (4) Sinatra’s three files (routes, layout, view), you can build a lot. Keep this playbook close, and iterate.
Top comments (0)