Tina4 Ruby - Quick Reference#
🔥 Hot Tips
- Route files live in
src/routes/, templates insrc/templates/, static files insrc/public/ - GET routes are public by default; POST/PUT/PATCH/DELETE require a token (use
@noauthto override) - Return a
HashorArrayfrom a route block and it auto-detects as JSON - Run
tina4 serveto launch the dev server on port 7147 - Chain
.secureor.cacheon any route for auth and caching
Installation • Static Websites • Routing • Middleware • Templates • Sessions • SCSS • Environments • Authentication • Forms & Tokens • AJAX • OpenAPI • Databases • Database Results • Migrations • ORM • CRUD • REST Client • Testing • Services • Websockets • Queues • WSDL • GraphQL • Localization • HTML Builder • Events • Logging • Cache • Health • DI Container • Error Overlay • Dev Admin • CLI • MCP • FakeData
Installation#
# Install the tina4 CLI once. Windows: irm https://tina4.com/install.ps1 | iexcurl -fsSL https://tina4.com/install.sh | shtina4 init ruby my-appcd my-apptina4 serveThe server starts on port 7147. One gem. No dependency tree. More details on project setup and customization.
Static Websites#
Put .html templates in ./src/templates and assets in ./src/public.
<!-- src/templates/index.html --><h1>Hello Static World</h1>More details on static website routing.
Basic Routing#
Tina4::Router.get("/") do |request, response| response.html "<h1>Hello Tina4 Ruby</h1>"endTina4::Router.post("/api/items") do |request, response| name = request.body["name"] || "" response.json({ name: name }, 201)endTina4::Router.get("/users/{id:int}") do |request, response| id = request.params[:id] response.json({ user_id: id })endDrop route files in src/routes/. Tina4 discovers them at startup. Follow the links for basic routing and dynamic routing with typed parameters.
Middleware#
log_request = lambda do |request, response, next_handler| $stderr.puts "#{request.method} #{request.path}" next_handler.call(request, response)endTina4::Router.get("/api/data", middleware: "log_request") do |request, response| response.json({ data: [1, 2, 3] })end# Apply middleware to a groupTina4::Router.group("/api/admin", middleware: "require_auth") do Tina4::Router.get("/dashboard") do |request, response| response.json({ page: "admin dashboard" }) endendFollow the links for more on Middleware Declaration and Pattern Matching.
Template Rendering#
Put .html templates in ./src/templates and assets in ./src/public.
<!-- src/templates/greeting.html --><h1>Hello {{ name }}</h1>Tina4::Router.get("/") do |request, response| response.render("greeting.html", { name: "World" })endSessions#
Sessions start on their own. Every route handler receives request.session ready to use.
Tina4::Router.get("/session/set") do |request, response| request.session.set("name", "Joe") request.session.set("info", { list: ["one", "two", "three"] }) response.text "Session set."endTina4::Router.get("/session/get") do |request, response| name = request.session.get("name", "Guest") info = request.session.get("info", {}) response.json({ name: name, info: info })endSCSS Stylesheets#
Drop files in ./src/public/scss, Tina4 compiles them to ./src/public/css.
// src/public/scss/main.scss$primary: #2c3e50;body { background: $primary; color: white;}More details on CSS and SCSS.
Environments#
The .env file sits at the project root. Tina4 reads it at startup.
TINA4_DEBUG=trueTINA4_PORT=7147TINA4_DATABASE_URL=sqlite:///data/app.dbTINA4_LOG_LEVEL=ALLTINA4_API_KEY=ABC1234api_key = ENV["TINA4_API_KEY"] || "ABC1234"Access env vars programmatically:
Tina4::Env.load_env # Load .env file (auto on server start)Tina4::Env.get_env("TINA4_DATABASE_URL") # Get value or nilTina4::Env.get_env("PORT", "7147") # Get value with defaultTina4::Env.has_env?("TINA4_DEBUG") # true if setTina4::Env.require_env!("TINA4_DATABASE_URL") # Raises KeyError if missingTina4::Env.truthy?(ENV["TINA4_DEBUG"]) # true for "true", "1", "yes"Authentication#
Tina4 uses JWT tokens. Keys auto-generate in .keys/. GET routes are public. POST/PUT/PATCH/DELETE require a bearer token by default.
# Public login routeTina4::Router.post("/login") do |request, response| token = Tina4::Auth.get_token({ user_id: 1, role: "admin" }) response.json({ token: token })end# Secured GET route (chain .secure)Tina4::Router.get("/api/profile") do |request, response| response.json({ user: request.user })end.secure# Or use the @secured decorator# @securedTina4::Router.get("/api/account") do |request, response| response.json({ account: request.user })endHTML Forms and Tokens#
<form method="POST" action="/register"> <input name="email"> <button>Save</button></form>More details on posting form data.
AJAX and frond.js#
Tina4 ships with frond.js, a zero-dependency JavaScript library for AJAX calls, form submissions, and real-time WebSocket connections.
More details on available features.
OpenAPI and Swagger UI#
Visit http://localhost:7147/swagger, available when TINA4_DEBUG=true.
# List all users# @description Returns all registered users# @tags Users# @query int $page Page number (default: 1)Tina4::Router.get("/api/users") do |request, response| response.json({ users: [] })endFollow the links for more on Configuration, Usage and Metadata.
Databases#
# Configured via .env (default: sqlite:///data/app.db)# Or create a connection in code:db = Tina4::Database.new("sqlite://app.db")db = Tina4::Database.new("postgres://localhost:5432/myapp", pool: 5)Follow the links for more on Available Connections, Core Methods, Usage and Transactions.
Database Results#
result = db.fetch("SELECT * FROM users", [], limit: 3, skip: 1)array = result.to_a # Array of hashesjson = result.to_json # JSON stringcsv = result.to_csv # CSV stringLooking at detailed Usage will deepen your understanding.
Migrations#
tina4 migrate --create create_users_table-- src/migrations/20260313120000_create_users_table.sqlCREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT);tina4 migrateMigrations have rollback support and status tracking.
ORM#
ORM models live in src/orm/. Tina4 auto-loads every .rb file in that directory.
class User < Tina4::ORM integer_field :id, primary_key: true string_field :name string_field :email table_name "users"enduser = User.newuser.name = "Alice"user.email = "alice@example.com"user.savefound = User.find(1)found.name = "Alice Wonder"found.saveThe ORM handles insert-or-update in a single save call. See the Advanced Detail for the full picture.
CRUD#
Tina4::Router.get("/users/dashboard") do |request, response| users = User.all response.render("users/dashboard.html", { users: users })endMore details on how CRUD works.
Consuming REST APIs#
api = Tina4::API.new("https://api.example.com", auth_header: "Bearer xyz")result = api.get("/users/42")puts result.bodyMore details on POST bodies, authorization headers, and API responses.
Inline Testing#
Tina4 uses RSpec. Tests live in tests/. Every _spec.rb file is auto-discovered.
require "tina4"RSpec.describe "Math operations" do it "adds numbers" do expect(2 + 2).to eq(4) endendRun: tina4 test
Services#
class CacheWarmer < Tina4::Service def run Tina4::Log.info("Warming cache...") # Your background work here sleep 60 endendTina4::ServiceRunner.register(CacheWarmer.new)Tina4::ServiceRunner.start_allWebsockets#
Define WebSocket handlers the same way you define HTTP routes. The handler receives three arguments: connection, event, and data.
Tina4::Router.websocket "/ws/echo" do |connection, event, data| if event == :message connection.send("Echo: #{data}") endendQueues#
The file-based backend works out of the box. No Redis. No RabbitMQ.
queue = Tina4::Queue.new(topic: "emails")queue.push({ to: "alice@example.com", subject: "Welcome", body: "Your account is ready."})WSDL#
class Calculator < Tina4::WSDL service_url "http://localhost:7147/calculator" def add(a, b) { result: a + b } endendGraphQL#
Tina4 includes a built-in GraphQL engine. No external gems.
schema = Tina4::GraphQLSchema.newgql = Tina4::GraphQL.new(schema)gql.register_route # POST /graphql, GET /graphql (playground)POST queries to /graphql, or visit it in a browser for the GraphiQL IDE.
{ users(limit: 5) { id name email } }Full details on manual schema definition, mutations, variables, fragments, and programmatic usage.
Localization (i18n)#
Set TINA4_LOCALE in .env to change the framework language.
puts Tina4.t("server_stopped") # "Server stopped." (en)HTML Builder#
el = Tina4::HtmlElement.new("div", { class: "card" }, ["Hello"])el.to_s # => '<div class="card">Hello</div>'# Nesting with callcard = Tina4::HtmlElement.new("div").call( { class: "card" }, Tina4::HtmlElement.new("h2").call("Title"), Tina4::HtmlElement.new("p").call("Content"),)# Helper methodsinclude Tina4::HtmlHelpershtml = _div({ class: "card" }, _h1("Title"), _p("Description"))Events#
# Subscribe to an eventTina4::Events.on("user.created") do |payload| puts "New user: #{payload[:name]}"end# Subscribe once - auto-removes after first fireTina4::Events.once("app.boot") do |payload| puts "App booted at #{payload[:time]}"end# Emit an event anywhere in the appTina4::Events.emit("user.created", { name: "Alice", id: 42 })Events are synchronous by default. Emit inside a service to run them off the request thread.
Logging#
Tina4::Log.info("Server ready on port 7147")Tina4::Log.debug("Params: #{request.params.inspect}")Tina4::Log.warning("Deprecated method called")Tina4::Log.error("Database connection failed")Set TINA4_LOG_LEVEL=ALL in .env to see every level. Production default is INFO. Log output goes to stdout and to logs/tina4.log.
Response Cache#
# Cache for 60 seconds (default)Tina4::Router.get("/api/products") do |request, response| response.json({ products: Product.all })end.cache# Custom TTL in secondsTina4::Router.get("/api/summary") do |request, response| response.json({ total: Order.count })end.cache(300)# Cache is keyed on method + path + query string.# Clear all cached responses:Tina4::Cache.flushHealth Endpoint#
Tina4 registers /health automatically. No setup needed.
curl http://localhost:7147/health{ "status": "ok", "uptime": 142, "version": "3.10.20" }The response includes framework version, uptime in seconds, and database connectivity when a TINA4_DATABASE_URL is configured. Use this endpoint for container liveness and readiness probes.
DI Container#
# Register a service by nameTina4::Container.register(:mailer) { Mailer.new(ENV["TINA4_MAIL_HOST"]) }# Register a singleton (resolved once, reused everywhere)Tina4::Container.register(:config, singleton: true) { AppConfig.load }# Resolve anywhere in the appmailer = Tina4::Container.resolve(:mailer)mailer.send_welcome(user)Registrations are lazy: the block runs on first .resolve. Singletons cache the result for the lifetime of the process.
Error Overlay#
When TINA4_DEBUG=true, unhandled exceptions render an in-browser overlay.
TINA4_DEBUG=trueThe overlay shows the exception class, message, and a syntax-highlighted stack trace with source lines. It replaces the default HTML error page only in debug mode. In production the framework returns a plain 500 response and writes the full trace to the log.
Dev Admin#
http://localhost:7147/__devThe /__dev dashboard is available when TINA4_DEBUG=true. It lists every registered route (method, path, middleware, auth), active services, queue depths, and recent log lines. No configuration needed: visit the URL after tina4 serve starts.
CLI Commands#
tina4 init ruby my-project # Scaffold a new Ruby projecttina4 serve # Start dev server (port 7147, live reload)tina4 serve --port 8080 # Custom porttina4 migrate # Run pending SQL migrationstina4 migrate --create name # Generate a timestamped migration filetina4 test # Run the RSpec test suitetina4 build # Compile SCSS, bundle assets for productiontina4 routes # Print all registered routes to stdoutMCP Server#
Tina4 starts an MCP (Model Context Protocol) server automatically when TINA4_DEBUG=true.
http://localhost:7147/__mcp# Register a custom MCP toolTina4::MCP.register_tool("get_user") do |params| user = User.find(params["id"].to_i) { id: user.id, name: user.name }endAI agents connect to /__mcp and discover routes, ORM models, and registered tools. Disable with TINA4_MCP=false in .env.
FakeData#
fake = Tina4::FakeData.newputs fake.name # "Liam Torres"puts fake.email # "liam.torres@example.com"puts fake.phone # "+1-555-0147"puts fake.address # "12 Elm Street, Springfield"puts fake.company # "Bright Horizon Ltd"puts fake.paragraph # Lorem-style filler textputs fake.integer(1, 100) # Random integer between 1 and 100puts fake.uuid # "a3f1c2d4-..."# Seed for reproducible datafake = Tina4::FakeData.new(seed: 42)Use FakeData in tests and migrations to generate consistent fixture data without external gems.
📕 Download the book#
Tina4 for Ruby Developers (PDF): full reference, printable, with clickable table of contents and PDF outline. Regenerated with every release.