DEV Community

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: false where it makes sense - title, body, author_name shouldn't be blank at the database level.
  • Defaults for boolean and integer fields. A published column without a default creates a three-state boolean (true, false, nil), which causes headaches with ActiveRecord's where(published: false) queries. Setting default: false avoids that.
  • Indexes on query-heavy columns. The slug gets a unique index (since you'll look up articles by slug). published and published_at get indexes because you'll filter and sort on them.
  • published_at is nullable. A draft has no publication date. Kiro infers this from the presence of the published boolean - 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: true validations on title, body, and author_name match the null: false in 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. \A and \z anchor to the actual string boundaries (not line boundaries like ^ and $).
  • The callback generates the slug from the title. String#parameterize is a Rails method that converts "My First Blog Post" into "my-first-blog-post". The callback only fires on create, and only if slug is 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_at is set when published is true. Also add a validation that the title is unique within the scope of the author_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_published method uses validate (no s) instead of validates - that's the Rails API for custom validation logic. It calls errors.add to attach an error message to a specific field. The published? 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), and by_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 table articles (plural, snake_case), controller ArticlesController (plural), and route resources :articles. Kiro doesn't need you to specify these mappings - it infers them.
  • File placement. Models go in app/models/, controllers in app/controllers/, migrations in db/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 validates for declarative validations, validate for custom methods, uniqueness: { scope: ... } for compound uniqueness, and format: { 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.