Scaffolding
One command. Six files. A working CRUD feature with routes, templates, tests, and Swagger docs -- ready to run.
That is Tina4's scaffolding system. It generates the boilerplate you write by hand in every project: models, migrations, routes, forms, views, and tests. You describe what you want. The generators produce it.
The CRUD Generator
This is the generator most developers reach for first. It creates everything a feature needs in one shot.
tina4ruby generate crud Product --fields "name:string,price:float"That single command creates six files:
| # | File | Purpose |
|---|---|---|
| 1 | src/orm/product.rb | ORM model with typed fields |
| 2 | migrations/20260401_create_product.sql | UP migration (CREATE TABLE) |
| 3 | migrations/20260401_create_product.down.sql | DOWN migration (DROP TABLE) |
| 4 | src/routes/products.rb | CRUD routes with Swagger annotations |
| 5 | src/templates/products/form.html | Form template with typed inputs and form_token |
| 6 | src/templates/products/view.html | List and detail templates |
| 7 | spec/products_spec.rb | RSpec stubs for all CRUD operations |
What Each File Contains
The model maps the product table to a Ruby class:
require "tina4"
class Product < Tina4::ORM
table_name "product"
field :id, :integer
field :name, :string
field :price, :float
field :created_at, :datetime
field :updated_at, :datetime
endThe migration creates the table:
-- migrations/20260401_create_product.sql
CREATE TABLE product (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(255),
price FLOAT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);The down migration reverses it:
-- migrations/20260401_create_product.down.sql
DROP TABLE IF EXISTS product;The routes file wires up five endpoints with Swagger docs:
require "tina4"
require_relative "../orm/product"
Tina4.get "/api/products", description: "List all products" do |request, response|
products = Product.new.select
response.call(products)
end
Tina4.get "/api/products/:id", description: "Get a product by ID" do |request, response|
product = Product.new
product.id = request.params[:id]
product.load
response.call(product)
end
Tina4.post "/api/products", description: "Create a product" do |request, response|
product = Product.new(request.body)
product.save
response.call(product, 201)
end
Tina4.put "/api/products/:id", description: "Update a product" do |request, response|
product = Product.new(request.body)
product.id = request.params[:id]
product.save
response.call(product)
end
Tina4.delete "/api/products/:id", description: "Delete a product" do |request, response|
product = Product.new
product.id = request.params[:id]
product.delete
response.call(nil, 204)
endThe form template renders typed inputs with CSRF protection:
<form method="POST" action="/api/products">
<input type="hidden" name="form_token" value="{{ form_token }}">
<label>Name</label>
<input type="text" name="name" required>
<label>Price</label>
<input type="number" step="0.01" name="price" required>
<button type="submit">Save</button>
</form>The test file stubs out CRUD assertions:
require "tina4"
RSpec.describe "Products API" do
let(:client) { Tina4::App.new.test_client }
it "creates a product" do
response = client.post("/api/products", { name: "Widget", price: 9.99 })
expect(response.status).to eq(201)
end
it "lists products" do
response = client.get("/api/products")
expect(response.status).to eq(200)
end
it "gets a product" do
response = client.get("/api/products/1")
expect(response.status).to eq(200)
end
it "updates a product" do
response = client.put("/api/products/1", { name: "Updated Widget" })
expect(response.status).to eq(200)
end
it "deletes a product" do
response = client.delete("/api/products/1")
expect(response.status).to eq(204)
end
endRun It
After generating, run the migration and start the server:
tina4ruby migrate
tina4ruby serveOpen Swagger UI at http://localhost:7147/swagger and test every endpoint. The scaffolded code works out of the box.
Individual Generators
The CRUD generator calls several smaller generators under the hood. You can call each one directly when you need a single piece.
Model
tina4ruby generate model Product --fields "name:string,price:float"Creates three files: the ORM model (src/orm/product.rb), the UP migration, and the DOWN migration. No routes, no templates, no tests.
Route
tina4ruby generate route products --model ProductCreates one file: src/routes/products.rb with CRUD endpoints and Swagger annotations. The model must exist first.
Migration
tina4ruby generate migration add_category_to_productCreates two files: migrations/20260401_add_category_to_product.sql and migrations/20260401_add_category_to_product.down.sql. Both are empty stubs. You write the SQL.
Middleware
tina4ruby generate middleware AuthLogCreates one file with before and after stubs:
require "tina4"
Tina4.middleware :before do |request|
puts "Request: #{request.method} #{request.url}"
request
end
Tina4.middleware :after do |request, response|
puts "Response: #{response.status}"
response
endTest
tina4ruby generate test products --model ProductCreates one file: spec/products_spec.rb with RSpec CRUD stubs.
Form
tina4ruby generate form Product --fields "name:string,price:float"Creates one file: src/templates/products/form.html with typed inputs and form_token.
View
tina4ruby generate view Product --fields "name:string,price:float"Creates two templates: a list view and a detail view in src/templates/products/.
CRUD
tina4ruby generate crud Product --fields "name:string,price:float"Shorthand for running all generators at once: model, migration, route, form, view, and test.
Auth
tina4ruby generate authGenerates the full authentication scaffold: User model, migrations, login/register/logout routes, templates, and tests.
AutoCRUD
AutoCRUD automatically generates REST API endpoints from your ORM models:
GET /api/{table}— List with pagination (?limit=10&offset=0)GET /api/{table}/{id}— Get single recordPOST /api/{table}— Create recordPUT /api/{table}/{id}— Update recordDELETE /api/{table}/{id}— Delete record
Usage
Tina4::AutoCrud.register(User)
Tina4::AutoCrud.generate_routes(prefix: "/api")AutoCrud.register wires up a single model. generate_routes mounts the five standard endpoints automatically — no route files needed.
The Auth Generator
Authentication needs more than one file. The auth generator creates seven:
tina4ruby generate auth| # | File | Purpose |
|---|---|---|
| 1 | src/orm/user.rb | User model with hashed password field |
| 2 | migrations/20260401_create_user.sql | UP migration |
| 3 | migrations/20260401_create_user.down.sql | DOWN migration |
| 4 | src/routes/auth.rb | Login, register, logout routes |
| 5 | src/templates/auth/login.html | Login form |
| 6 | src/templates/auth/register.html | Registration form |
| 7 | spec/auth_spec.rb | Auth flow tests |
The generated routes handle password hashing, JWT token creation, and session management. The templates include CSRF tokens. The tests cover registration, login, invalid credentials, and logout.
Run the migration, start the server, and you have working auth:
tina4ruby migrate
tina4ruby serveField Types
Generators accept these field types. Each type maps to a specific column type in migrations, input type in forms, and display format in views.
| Field Type | Migration Column | Form Input | View Display |
|---|---|---|---|
string | VARCHAR(255) | <input type="text"> | Plain text |
int | INTEGER | <input type="number"> | Number |
float | FLOAT | <input type="number" step="0.01"> | Decimal |
bool | BOOLEAN | <input type="checkbox"> | Yes / No |
text | TEXT | <textarea> | Paragraph |
datetime | DATETIME | <input type="datetime-local"> | Formatted date |
blob | BLOB | <input type="file"> | Download link |
Table Naming Convention
Tina4 uses singular table names. The model name Product maps to the table product. The model name OrderItem maps to order_item. The generator handles the conversion.
Combining Generators
Sometimes you want a model with routes but no form. Or a model with a migration but no test. The --with flags let you compose:
tina4ruby generate model Product --fields "name:string,price:float" --with-route --with-migrationAvailable flags:
| Flag | Adds |
|---|---|
--with-route | CRUD route file |
--with-migration | Migration files (included by default with model) |
--with-test | Test file |
--with-form | Form template |
--with-view | View templates |
The generate crud command is equivalent to using all --with flags at once.
Exercise: Scaffold a Blog
Build a blog with three resources using generators.
Step 1: Generate the auth system.
tina4ruby generate authStep 2: Scaffold the Post resource.
tina4ruby generate crud Post --fields "title:string,body:text,published:bool"Step 3: Scaffold the Category resource.
tina4ruby generate crud Category --fields "name:string,description:text"Step 4: Add a migration to link posts to categories.
tina4ruby generate migration add_category_id_to_postEdit the migration to add the foreign key:
ALTER TABLE post ADD COLUMN category_id INTEGER REFERENCES category(id);Step 5: Run all migrations and start the server.
tina4ruby migrate
tina4ruby serveYou now have a working blog with authentication, posts, categories, and Swagger documentation. Total commands: five. Total hand-written SQL: one line.
Gotchas
Generators Do Not Overwrite
If a file exists, the generator skips it and prints a warning. This protects your edits. To regenerate, delete the file first.
Run Migrate After Generate
The model generator creates migration files. Those files do nothing until you run tina4ruby migrate. Generate and migrate are separate steps by design.
File Naming Matters
The generator derives file names from the model name. Product becomes product.rb for the model and products.rb for routes. Do not rename generated files unless you update all requires.
Field Changes Need New Migrations
Changing --fields and re-running the generator does not update existing migrations. Create a new migration with generate migration and write the ALTER TABLE by hand.
Singular Table Names
Tina4 uses singular table names: product, not products. The route paths use plural (/api/products), but the table stays singular. The generator handles this split.