The Missing Config Files#
In a TypeScript project, setting up a new API endpoint means: create a route file, import the handler, configure the router, create the controller, import the model, configure the ORM connection, and wire everything together. In Rails, you run one command:
rails generate scaffold Url target_url:string short_code:string title:string
And you get: a model, a migration, a controller with all CRUD actions, views, routes, tests, and helpers. All wired together. No config.
This is “convention over configuration” — the philosophy Rails is built on. Instead of telling the framework how to connect things, you follow naming conventions and the framework connects them for you.
The Convention That Replaces Configuration#
In Express or Spring, you explicitly wire every connection:
// Express: you tell the framework everything
import { Router } from "express";
import { UrlController } from "../controllers/url.controller";
const router = Router();
router.get("/urls", UrlController.index);
router.get("/urls/:id", UrlController.show);
router.post("/urls", UrlController.create);
export default router;
In Rails, the convention IS the configuration:
# config/routes.rb
resources :urls
# This single line creates ALL seven RESTful routes:
# GET /urls -> UrlsController#index
# GET /urls/:id -> UrlsController#show
# GET /urls/new -> UrlsController#new
# POST /urls -> UrlsController#create
# GET /urls/:id/edit -> UrlsController#edit
# PATCH /urls/:id -> UrlsController#update
# DELETE /urls/:id -> UrlsController#destroy
The controller is named UrlsController (plural of the model). The model is Url. The table is urls. The views are in app/views/urls/. You don’t configure these relationships — you follow the naming convention.
MVC: Same Pattern, Less Ceremony#
If you’ve used Express with a service layer or Spring MVC, Rails MVC is the same pattern with less boilerplate:
# app/controllers/urls_controller.rb
class UrlsController < ApplicationController
def index
@urls = Url.all
# Rails automatically renders app/views/urls/index.html.erb
end
def show
@url = Url.find(params[:id])
# Rails automatically renders app/views/urls/show.html.erb
end
def create
@url = Url.new(url_params)
if @url.save
redirect_to @url, notice: "Created!"
else
render :new, status: :unprocessable_entity
end
end
private
def url_params
params.require(:url).permit(:target_url) # strong params — whitelist allowed fields
end
end
No imports. No res.json(). No @GetMapping. The controller name matches the route. Instance variables (@urls) are automatically available in the view. The view file is found by convention (controller name + action name).
ActiveRecord: The ORM That Feels Like Magic#
ActiveRecord is Rails’ ORM. If you’ve used Prisma, TypeORM, or JPA/Hibernate, the concept is familiar — but the syntax is more concise.
# Ruby (ActiveRecord)
Url.all # SELECT * FROM urls
Url.find(1) # SELECT * FROM urls WHERE id = 1
Url.where(status: "active") # SELECT * FROM urls WHERE status = 'active'
Url.where("clicks > ?", 100) # parameterized query
Url.order(created_at: :desc).limit(10) # ORDER BY created_at DESC LIMIT 10
Url.find_by(short_code: "abc123") # first match or nil
// TypeScript (Prisma)
await prisma.url.findMany();
await prisma.url.findUnique({ where: { id: 1 } });
await prisma.url.findMany({ where: { status: "active" } });
await prisma.url.findMany({ where: { clicks: { gt: 100 } } });
await prisma.url.findMany({ orderBy: { createdAt: "desc" }, take: 10 });
await prisma.url.findFirst({ where: { shortCode: "abc123" } });
The key differences:
| Aspect | ActiveRecord | Prisma/TypeORM |
|---|---|---|
| Query style | Method chaining on model class | Method on client with object params |
| Associations | has_many :orders | Schema-level relations |
| Validations | In the model (validates :email, presence: true) | Zod/class-validator (separate) |
| Migrations | rails generate migration AddStatusToUrls status:string | prisma migrate dev / TypeORM migrations |
| Null handling | Returns nil or raises | Returns null or throws |
ActiveRecord models are also where validations live:
class Url < ApplicationRecord
validates :target_url, presence: true, format: { with: URI::DEFAULT_PARSER.make_regexp }
validates :short_code, presence: true, uniqueness: true, length: { is: 6 }
before_create :generate_short_code
private
def generate_short_code
self.short_code = SecureRandom.alphanumeric(6)
end
end
In TypeScript, validation logic usually lives in a separate layer (Zod schemas, middleware). In Rails, the model handles it. This is the “fat model, skinny controller” convention — business logic lives in models, controllers just coordinate.
Migrations: Schema as Code#
rails generate migration CreateUrls target_url:string short_code:string clicks:integer
This generates:
class CreateUrls < ActiveRecord::Migration[7.1]
def change
create_table :urls do |t|
t.string :target_url, null: false
t.string :short_code, null: false
t.integer :clicks, default: 0
t.timestamps # created_at and updated_at automatically
end
add_index :urls, :short_code, unique: true
end
end
Run with rails db:migrate. Rollback with rails db:rollback. The migration history lives in db/schema.rb — a snapshot of the current database state.
Same concept as Prisma migrations or Flyway/Liquibase in Java, but generated from the command line based on a naming convention: Add{Column}To{Table} or Create{Table} tells the generator what to scaffold.
The Scaffold: Your Starting Point#
rails generate scaffold Url target_url:string short_code:string title:string clicks_count:integer
This creates everything: model, migration, controller (with all 7 CRUD actions), views, routes, tests, and helpers. It’s a complete working feature in one command.
The convention is to scaffold first, then customize. Delete the actions you don’t need. Add validations to the model. Replace generated views with your design. It’s faster to trim than to build from scratch.
# Want to undo? The destroyer removes exactly what the generator created
rails destroy scaffold Url
The Honest Reaction#
Coming from TypeScript, my first reaction to Rails conventions was suspicion. Where’s the explicit wiring? How do I know what’s connected to what? What if the convention doesn’t match my use case?
After a short while, the suspicion faded. The conventions are predictable — once you know the patterns, you can read any Rails codebase and immediately know where things are. UrlsController means there’s a Url model and a urls table. app/views/urls/show.html.erb renders when you hit GET /urls/:id. No config to trace.
The next post covers where Rails diverges more sharply from the Node.js ecosystem: Hotwire instead of React, service objects, and Rack middleware.
