Kiro for Ruby: Generating Rails Controllers and Models
The Setup
Kiro supports Ruby as one of its officially listed programming languages. It's built on VS Code, so your existing Ruby and Rails extensions, themes, and keybindings carry over. Open your Rails project in Kiro and you're ready to go.
Kiro offers two session types: Vibe sessions for conversational coding, and Spec sessions for structured development (requirements, design, then implementation tasks). For generating a straightforward resource like this, a Vibe session works well. For a more complex feature - say, a full publishing workflow with drafts, scheduling, and notifications - you'd use a Spec session to plan before implementing.
Generating the Resource
In a Vibe session, you describe what you want:
Create an Article resource for a blog application. The article should have: title (string), body (text), author_name (string), published (boolean, default false), slug (string), view_count (integer, default 0), and published_at (datetime, nullable). Generate the model, migration, controller with full RESTful actions, and routes.
Kiro reads your existing project structure - your Gemfile, existing models, route file, controller patterns - and generates code that fits. It doesn't produce code in a vacuum; it matches the conventions already in your codebase.
The Migration
Kiro generates a migration that follows Rails naming conventions - timestamped filename, create_ prefix, plural table name:
# db/migrate/20260706120000_create_articles.rb
class CreateArticles < ActiveRecord::Migration[7.1]
def change
create_table :articles do |t|
t.string :title, null: false
t.text :body, null: false
t.string :author_name, null: false
t.boolean :published, default: false, null: false
t.string :slug, null: false
t.integer :view_count, default: 0, null: false
t.datetime :published_at
t.timestamps
end
add_index :articles, :slug, unique: true
add_index :articles, :published
add_index :articles, :published_at
end
end
A few things to note:
- Null constraints on required fields. Rails developers know that database-level constraints complement model validations. Kiro adds
null: falsewhere it makes sense -title,body,author_nameshouldn't be blank at the database level. - Defaults for boolean and integer fields. A
publishedcolumn without a default creates a three-state boolean (true, false, nil), which causes headaches with ActiveRecord'swhere(published: false)queries. Settingdefault: falseavoids that. - Indexes on query-heavy columns. The
sluggets a unique index (since you'll look up articles by slug).publishedandpublished_atget indexes because you'll filter and sort on them. published_atis nullable. A draft has no publication date. Kiro infers this from the presence of thepublishedboolean - if there's a separate "is published?" flag, the timestamp should be optional.
This is the kind of migration a developer with a few years of Rails experience would write. Not the bare minimum, but not over-engineered either.
The Model
# app/models/article.rb
class Article < ApplicationRecord
# Validations
validates :title, presence: true, length: { maximum: 255 }
validates :body, presence: true
validates :author_name, presence: true, length: { maximum: 100 }
validates :slug, presence: true, uniqueness: true, format: { with: /\A[a-z0-9\-]+\z/ }
validates :view_count, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
# Callbacks
before_validation :generate_slug, on: :create
private
def generate_slug
self.slug = title.parameterize if title.present? && slug.blank?
end
end
This model demonstrates several ActiveRecord conventions:
- Validations mirror the database constraints. The
presence: truevalidations ontitle,body, andauthor_namematch thenull: falsein the migration. Belt and suspenders - the database constraint catches anything that slips past the model layer. - Length limits are explicit. Without
length: { maximum: 255 }, a user could submit a 10,000-character title. The string column type in most databases truncates at 255 characters anyway, so the validation makes the limit visible in Ruby. - The slug format uses a regex. The
/\A[a-z0-9\-]+\z/pattern ensures slugs are URL-safe: lowercase letters, numbers, and hyphens only.\Aand\zanchor to the actual string boundaries (not line boundaries like^and$). - The callback generates the slug from the title.
String#parameterizeis a Rails method that converts "My First Blog Post" into "my-first-blog-post". The callback only fires on create, and only ifslugis blank - so you can still set a custom slug if you want.
The Controller
# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
before_action :set_article, only: %i[show edit update destroy]
def index
@articles = Article.all.order(created_at: :desc)
end
def show
end
def new
@article = Article.new
end
def create
@article = Article.new(article_params)
if @article.save
redirect_to @article, notice: "Article was successfully created."
else
render :new, status: :unprocessable_entity
end
end
def edit
end
def update
if @article.update(article_params)
redirect_to @article, notice: "Article was successfully updated."
else
render :edit, status: :unprocessable_entity
end
end
def destroy
@article.destroy
redirect_to articles_url, notice: "Article was successfully destroyed."
end
private
def set_article
@article = Article.find(params[:id])
end
def article_params
params.require(:article).permit(:title, :body, :author_name, :slug, :published, :published_at)
end
end
Let's break down what Rails conventions are at work here:
RESTful Routing - The controller has exactly seven actions: index, show, new, create, edit, update, destroy. These map to the standard HTTP verbs and URL patterns that resources :articles generates in Rails:
| Action | HTTP Verb | Path | Purpose |
|---|---|---|---|
| index | GET | /articles | List all articles |
| show | GET | /articles/:id | Show one article |
| new | GET | /articles/new | Render create form |
| create | POST | /articles | Save new article |
| edit | GET | /articles/:id/edit | Render edit form |
| update | PATCH/PUT | /articles/:id | Update article |
| destroy | DELETE | /articles/:id | Delete article |
No custom actions, no non-RESTful routes. This is Rails' convention-over-configuration in its purest form.
Strong Parameters - The article_params method uses params.require(:article).permit(...) to whitelist exactly which attributes can be mass-assigned. This is Rails' defense against mass-assignment vulnerabilities. Notice that view_count is deliberately excluded - it shouldn't be user-settable through a form submission. Only title, body, author_name, slug, published, and published_at are permitted.
before_action and DRY - The set_article callback runs before show, edit, update, and destroy - the four actions that operate on an existing record. This avoids repeating @article = Article.find(params[:id]) in each action.
Status Codes on Render - The render :new, status: :unprocessable_entity on validation failure returns HTTP 422. This is important for Turbo (Hotwire) in Rails 7+ - Turbo expects a 422 status to know that it should replace the form with the re-rendered version showing validation errors.
The Routes
# config/routes.rb (addition)
resources :articles
One line. That's it. resources :articles generates all seven RESTful routes mapping to the seven controller actions. This is the convention-over-configuration payoff - because the controller follows the naming convention (ArticlesController with standard action names), and the model follows its convention (Article mapping to the articles table), one line of routing config wires everything together.
Adding Validations
Now let's ask Kiro to add more business logic. In the same Vibe session:
Add a validation that ensures
published_atis set whenpublishedis true. Also add a validation that the title is unique within the scope of theauthor_name.
Kiro updates the model:
# app/models/article.rb
class Article < ApplicationRecord
# Validations
validates :title, presence: true, length: { maximum: 255 }
validates :title, uniqueness: { scope: :author_name, message: "has already been used by this author" }
validates :body, presence: true
validates :author_name, presence: true, length: { maximum: 100 }
validates :slug, presence: true, uniqueness: true, format: { with: /\A[a-z0-9\-]+\z/ }
validates :view_count, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validate :published_at_required_when_published
# Callbacks
before_validation :generate_slug, on: :create
private
def generate_slug
self.slug = title.parameterize if title.present? && slug.blank?
end
def published_at_required_when_published
if published? && published_at.blank?
errors.add(:published_at, "must be set when article is published")
end
end
end
Two things to highlight:
- Scoped uniqueness.
uniqueness: { scope: :author_name }means two different authors can both have an article titled "Getting Started," but the same author can't. The scope creates a compound uniqueness check. - Custom validation method. The
published_at_required_when_publishedmethod usesvalidate(nos) instead ofvalidates- that's the Rails API for custom validation logic. It callserrors.addto attach an error message to a specific field. Thepublished?method is automatically generated by ActiveRecord for boolean columns.
Adding Scopes
Next request:
Add scopes to the Article model:
published(where published is true, ordered by published_at descending),drafts(where published is false), andby_author(author_name)that filters by author.
Kiro adds the scopes:
# app/models/article.rb
class Article < ApplicationRecord
# Scopes
scope :published, -> { where(published: true).order(published_at: :desc) }
scope :drafts, -> { where(published: false) }
scope :by_author, ->(name) { where(author_name: name) }
# Validations
validates :title, presence: true, length: { maximum: 255 }
validates :title, uniqueness: { scope: :author_name, message: "has already been used by this author" }
validates :body, presence: true
validates :author_name, presence: true, length: { maximum: 100 }
validates :slug, presence: true, uniqueness: true, format: { with: /\A[a-z0-9\-]+\z/ }
validates :view_count, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validate :published_at_required_when_published
# Callbacks
before_validation :generate_slug, on: :create
private
def generate_slug
self.slug = title.parameterize if title.present? && slug.blank?
end
def published_at_required_when_published
if published? && published_at.blank?
errors.add(:published_at, "must be set when article is published")
end
end
end
Scopes in Rails are class-level methods that return ActiveRecord::Relation objects. Because they return relations, they're chainable:
# Find published articles by a specific author
Article.published.by_author("Jane Smith")
# Find draft articles, most recent first
Article.drafts.order(created_at: :desc)
# Count published articles
Article.published.count
Kiro uses the lambda syntax (-> { ... }) which is standard Rails practice for scopes. The by_author scope takes an argument, so its lambda accepts a parameter. All three scopes produce SQL that hits the indexes we defined in the migration (published and published_at).
What Kiro Understands About Rails Conventions
Through this example, Kiro demonstrated awareness of several Rails conventions:
- Naming conventions. Model name
Article(singular, CamelCase) maps to tablearticles(plural, snake_case), controllerArticlesController(plural), and routeresources :articles. Kiro doesn't need you to specify these mappings - it infers them. - File placement. Models go in
app/models/, controllers inapp/controllers/, migrations indb/migrate/. The migration filename includes a timestamp. These are non-negotiable in Rails, and Kiro follows them. - RESTful design. Seven standard actions, no custom routes for basic CRUD. If you need non-RESTful behavior, you'd ask for it - but the default is the conventional seven.
- Strong parameters. Whitelisting at the controller level, excluding fields that shouldn't be mass-assigned (
view_count,id,timestamps). - Database-level safety. Null constraints, defaults, and indexes in the migration complement the model validations. Kiro treats the database schema as a contract, not just a storage mechanism.
- Validation API. Using
validatesfor declarative validations,validatefor custom methods,uniqueness: { scope: ... }for compound uniqueness, andformat: { with: ... }for regex patterns.
Using Steering for Team Conventions
If your team has specific Rails conventions - say you always use UUIDs as primary keys, or you prefer frozen_string_literal comments at the top of every file, or you use Pundit for authorization - you can codify those in a Kiro steering file. Create a file at .kiro/steering/rails-conventions.md:
# Rails Conventions
Comments
No comments yet. Start the discussion.