Kemal ORM Integration (Crecto)
This skill provides expert guidance on using Crecto ORM with Kemal applications and SQLite, strictly following patterns from kemal-by-example/budget-management-orm.
Core Mandates
Dependencies: Include
kemal,crecto, andsqlite3inshard.ymlandrequirethem in the main application file.Repository Setup: Centralize Crecto configuration in a
config/repo.crmodule:module MyProject module Repo extend Crecto::Repo config do |conf| conf.adapter = Crecto::Adapters::SQLite3 conf.db = ENV["DATABASE_URL"]? || "sqlite3:./db/app.db" end end endSchema Initialization: Set up database schema tables with
Repo.db_adapter.execin aSchema.setupmodule:module MyProject module Schema extend self def setup Repo.db_adapter.exec <<-SQL CREATE TABLE IF NOT EXISTS budget_entries ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, kind TEXT NOT NULL, amount_cents INTEGER NOT NULL, notes TEXT NOT NULL DEFAULT '', created_at DATETIME, updated_at DATETIME ); SQL end end endModel Definition: Subclass
Crecto::Model, declare fields insideschema, and set validation rules:module MyProject class BudgetEntry < Crecto::Model schema "budget_entries" do field :title, String field :kind, String field :amount_cents, Int64 field :notes, String end validate_required :title validate_inclusion :kind, %w[income expense] end endRepository Operations:
- Querying all:
query = Crecto::Repo::Query.new.order_by("id DESC"); Repo.all(BudgetEntry, query) - Finding by ID:
Repo.get(BudgetEntry, id) - Creating:
entry = BudgetEntry.new; entry.title = title; Repo.insert(entry) - Updating:
entry.title = new_title; Repo.update(entry) - Deleting:
Repo.delete(entry)
- Querying all:
Patterns from Source Code
Repo Configuration (config/repo.cr)
require "crecto"
module BudgetManagementOrm
module Repo
extend Crecto::Repo
config do |conf|
conf.adapter = Crecto::Adapters::SQLite3
conf.db = ENV["DATABASE_URL"]? || "sqlite3:./db/budget.db"
end
end
end
Schema Setup (config/schema.cr)
require "./repo"
module BudgetManagementOrm
module Schema
extend self
def setup
Repo.db_adapter.exec <<-SQL
CREATE TABLE IF NOT EXISTS budget_entries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
kind TEXT NOT NULL,
amount_cents INTEGER NOT NULL,
notes TEXT NOT NULL DEFAULT '',
created_at DATETIME,
updated_at DATETIME
);
SQL
end
end
end
Model with Validations & Domain Logic (models/budget_entry.cr)
require "../config/repo"
module BudgetManagementOrm
class BudgetEntry < Crecto::Model
schema "budget_entries" do
field :title, String
field :kind, String
field :amount_cents, Int64
field :notes, String
end
validate_required :title
validate_inclusion :kind, %w[income expense]
def income? : Bool
kind == "income"
end
def expense? : Bool
kind == "expense"
end
def self.all_ordered : Array(BudgetEntry)
query = Crecto::Repo::Query.new.order_by("id DESC")
Repo.all(BudgetEntry, query)
end
def self.find(id : Int64) : BudgetEntry?
Repo.get(BudgetEntry, id)
end
def self.create(title : String, kind : String, notes : String, amount_cents : Int64)
entry = BudgetEntry.new
entry.title = title
entry.kind = kind
entry.notes = notes
entry.amount_cents = amount_cents
Repo.insert(entry)
end
def update_fields(title : String, kind : String, notes : String, amount_cents : Int64)
self.title = title
self.kind = kind
self.notes = notes
self.amount_cents = amount_cents
Repo.update(self)
end
def destroy
Repo.delete(self)
end
end
end
Route Integration with Crecto Models (routes/entries.cr)
get "/entries" do
entries = BudgetManagementOrm::BudgetEntry.all_ordered
render "src/views/entries/index.ecr", "src/views/layouts/application.ecr"
end
get "/entries/:id/edit" do |env|
id = env.params.url["id"]?.try(&.to_i64?)
entry = id ? BudgetManagementOrm::BudgetEntry.find(id) : nil
if entry
render "src/views/entries/edit.ecr", "src/views/layouts/application.ecr"
else
env.response.status = :not_found
"Entry not found"
end
end
post "/entries" do |env|
title = env.params.body["title"]?.try(&.strip) || ""
raw_kind = env.params.body["kind"]? || "expense"
kind = raw_kind == "income" ? "income" : "expense"
notes = env.params.body["notes"]?.try(&.strip) || ""
cents = BudgetManagementOrm::Money.parse_cents(env.params.body["amount"]? || "")
if !title.empty? && cents && cents > 0
BudgetManagementOrm::BudgetEntry.create(title, kind, notes, cents)
end
env.redirect "/entries"
end
post "/entries/:id/delete" do |env|
id = env.params.url["id"]?.try(&.to_i64?)
entry = id ? BudgetManagementOrm::BudgetEntry.find(id) : nil
entry.try(&.destroy)
env.redirect "/entries"
end
Best Practices
- Centwise Currency Storage: Always store monetary values as integers (
Int64cents) to avoid floating-point rounding errors. - Repository Pattern: Delegate database access to Crecto's
Repoobject rather than executing raw SQL strings directly in route handlers. - Model Validation: Use
validate_required,validate_format, andvalidate_inclusioninside model definitions for declarative data integrity. - Initialization Order: Run
Schema.setupin your main entrypoint file before callingKemal.run.
When to Use
- When building Kemal applications that require ORM abstraction (Crecto) instead of raw SQL strings.
- When organizing complex domain models with validations and relations.
- When managing structured database queries with ordering, filtering, and transactions.