DEV Community

8rowni3
8rowni3

Posted on

The Ruby & Sinatra Playbook from Core Concepts to Tiny Web Apps

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

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

Truthiness: nil and false are falsey; everything else is truthy.

Strings & symbols

greeting = "Hi, #{name}"  # interpolation
:ready == :ready          # symbols are interned
Enter fullscreen mode Exit fullscreen mode

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

Multiple assignment & splats

a, b = [10, 20]          # a=10, b=20
head, *rest = [1,2,3,4]  # head=1, rest=[2,3,4]
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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

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

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

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

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

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

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>
Enter fullscreen mode Exit fullscreen mode
<!-- views/index.erb -->
<h1>Currency pairs</h1>
<ul>
  <% @codes.each do |code| %>
    <li><a href="/<%= code %>">Convert 1 <%= code %> to...</a></li>
  <% end %>
</ul>
Enter fullscreen mode Exit fullscreen mode
<!-- 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>
Enter fullscreen mode Exit fullscreen mode
<!-- views/convert.erb -->
<h1>Convert <%= @from %> to <%= @to %></h1>
<p>1 <%= @from %> equals <%= @rate %> <%= @to %>.</p>
<p>Go <a href="/<%= @from %>">back</a>.</p>
Enter fullscreen mode Exit fullscreen mode

ASCII request flow

Browser ──▶ Sinatra Route ──▶ (optional) Helper/Service ──▶ HTTP API
   │             │                            │               │
   └─────────────┴────────── render(ERB) ◀────┴──── JSON ◀────┘

Enter fullscreen mode Exit fullscreen mode

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

Run:

ruby test_app.rb
Enter fullscreen mode Exit fullscreen mode

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

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

Refinements (scoped monkey-patch)

module Titleize
  refine String do
    def titleize = split.map!(&:capitalize).join(" ")
  end
end

using Titleize
"hello world".titleize # "Hello World"
Enter fullscreen mode Exit fullscreen mode

Lazy enumerators (streaming style)

(1..Float::INFINITY).lazy.select(&:odd?).map { |n| n*n }.first(5)
# => [1, 9, 25, 49, 81]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • If using environment variables: add them in the provider’s dashboard (e.g., EXCHANGE_RATE_KEY) and use ENV.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 and Convert FROM to TO
    • have “back” links to / and /FROM respectively
  • [ ] 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 }
Enter fullscreen mode Exit fullscreen mode

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)