Skip to content

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.

bash
tina4ruby generate crud Product --fields "name:string,price:float"

That single command creates six files:

#FilePurpose
1src/orm/product.rbORM model with typed fields
2migrations/20260401_create_product.sqlUP migration (CREATE TABLE)
3migrations/20260401_create_product.down.sqlDOWN migration (DROP TABLE)
4src/routes/products.rbCRUD routes with Swagger annotations
5src/templates/products/form.htmlForm template with typed inputs and form_token
6src/templates/products/view.htmlList and detail templates
7spec/products_spec.rbRSpec stubs for all CRUD operations

What Each File Contains

The model maps the product table to a Ruby class:

ruby
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
end

The migration creates the table:

sql
-- 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:

sql
-- migrations/20260401_create_product.down.sql
DROP TABLE IF EXISTS product;

The routes file wires up five endpoints with Swagger docs:

ruby
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)
end

The form template renders typed inputs with CSRF protection:

html
<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:

ruby
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
end

Run It

After generating, run the migration and start the server:

bash
tina4ruby migrate
tina4ruby serve

Open 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

bash
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

bash
tina4ruby generate route products --model Product

Creates one file: src/routes/products.rb with CRUD endpoints and Swagger annotations. The model must exist first.

Migration

bash
tina4ruby generate migration add_category_to_product

Creates 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

bash
tina4ruby generate middleware AuthLog

Creates one file with before and after stubs:

ruby
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
end

Test

bash
tina4ruby generate test products --model Product

Creates one file: spec/products_spec.rb with RSpec CRUD stubs.

Form

bash
tina4ruby generate form Product --fields "name:string,price:float"

Creates one file: src/templates/products/form.html with typed inputs and form_token.

View

bash
tina4ruby generate view Product --fields "name:string,price:float"

Creates two templates: a list view and a detail view in src/templates/products/.

CRUD

bash
tina4ruby generate crud Product --fields "name:string,price:float"

Shorthand for running all generators at once: model, migration, route, form, view, and test.

Auth

bash
tina4ruby generate auth

Generates 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 record
  • POST /api/{table} — Create record
  • PUT /api/{table}/{id} — Update record
  • DELETE /api/{table}/{id} — Delete record

Usage

ruby
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:

bash
tina4ruby generate auth
#FilePurpose
1src/orm/user.rbUser model with hashed password field
2migrations/20260401_create_user.sqlUP migration
3migrations/20260401_create_user.down.sqlDOWN migration
4src/routes/auth.rbLogin, register, logout routes
5src/templates/auth/login.htmlLogin form
6src/templates/auth/register.htmlRegistration form
7spec/auth_spec.rbAuth 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:

bash
tina4ruby migrate
tina4ruby serve

Field 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 TypeMigration ColumnForm InputView Display
stringVARCHAR(255)<input type="text">Plain text
intINTEGER<input type="number">Number
floatFLOAT<input type="number" step="0.01">Decimal
boolBOOLEAN<input type="checkbox">Yes / No
textTEXT<textarea>Paragraph
datetimeDATETIME<input type="datetime-local">Formatted date
blobBLOB<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:

bash
tina4ruby generate model Product --fields "name:string,price:float" --with-route --with-migration

Available flags:

FlagAdds
--with-routeCRUD route file
--with-migrationMigration files (included by default with model)
--with-testTest file
--with-formForm template
--with-viewView 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.

bash
tina4ruby generate auth

Step 2: Scaffold the Post resource.

bash
tina4ruby generate crud Post --fields "title:string,body:text,published:bool"

Step 3: Scaffold the Category resource.

bash
tina4ruby generate crud Category --fields "name:string,description:text"

Step 4: Add a migration to link posts to categories.

bash
tina4ruby generate migration add_category_id_to_post

Edit the migration to add the foreign key:

sql
ALTER TABLE post ADD COLUMN category_id INTEGER REFERENCES category(id);

Step 5: Run all migrations and start the server.

bash
tina4ruby migrate
tina4ruby serve

You 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.

Sponsored with 🩵 by Code InfinityCode Infinity