Forms & Validations in Rails
<p>Forms are where users hand your app messy, incomplete, or malicious input. Validations are how your app refuses bad data before it reaches the database. If you’re building AI features, this matters even more. Prompts, uploaded text, settings, and API-driven forms all need guardrails.</p> <p>In this post, we’ll build a simple document form in Rails and validate it properly.</p> <h2> Generate a resource </h2> <p>If you’ve been following along, you already have a <code>Document</code> model. Let’s assume it looks like this:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight ruby"><code><span class="k">class</span> <span class="nc">Document</span> <span class="o"><</span> <span class="no">ApplicationRecord</span> <span class="n">belongs_to</span> <span class="ss">:proj
Forms are where users hand your app messy, incomplete, or malicious input. Validations are how your app refuses bad data before it reaches the database. If you’re building AI features, this matters even more. Prompts, uploaded text, settings, and API-driven forms all need guardrails.
In this post, we’ll build a simple document form in Rails and validate it properly.
Generate a resource
If you’ve been following along, you already have a Document model. Let’s assume it looks like this:
class Document < ApplicationRecord belongs_to :project endclass Document < ApplicationRecord belongs_to :project endEnter fullscreen mode
Exit fullscreen mode
And the table has these columns:
-
project_id
-
title
-
body
-
status
Now create a controller if you don’t already have one:
bin/rails generate controller Documents new create edit update
Enter fullscreen mode
Exit fullscreen mode
Add routes in config/routes.rb:
Rails.application.routes.draw do resources :projects do resources :documents, only: [:new, :create, :edit, :update] end endRails.application.routes.draw do resources :projects do resources :documents, only: [:new, :create, :edit, :update] end endEnter fullscreen mode
Exit fullscreen mode
Nested routes make sense here because a document belongs to a project.
Add model validations
Start with the model. Put the rules close to the data.
app/models/document.rb
class Document < ApplicationRecord belongs_to :projectclass Document < ApplicationRecord belongs_to :projectvalidates :title, presence: true, length: { maximum: 120 } validates :body, presence: true, length: { minimum: 20 } validates :status, presence: true, inclusion: { in: %w[draft published archived] } end`
Enter fullscreen mode
Exit fullscreen mode
This gives you three useful protections:
-
no blank titles
-
no tiny document bodies
-
no random status values like done or weird
Test it in the console:
bin/rails console
Enter fullscreen mode
Exit fullscreen mode
doc = Document.new(title: "", body: "short", status: "wat") doc.valid?doc = Document.new(title: "", body: "short", status: "wat") doc.valid?=> false
doc.errors.full_messages
=> [blocked]
"Title can't be blank",
"Body is too short (minimum is 20 characters)",
"Status is not included in the list"
]`
Enter fullscreen mode
Exit fullscreen mode
That’s the contract your form will rely on.
Build the controller with strong parameters
Strong parameters are Rails’ way of saying: only accept the fields I explicitly allow.
app/controllers/documents_controller.rb
class DocumentsController < ApplicationController before_action :set_project before_action :set_document, only: [:edit, :update]class DocumentsController < ApplicationController before_action :set_project before_action :set_document, only: [:edit, :update]def new @document = @project.documents.new(status: "draft") end
def create @document = @project.documents.new(document_params)
if @document.save redirect_to @project, notice: "Document created." else render :new, status: :unprocessable_entity end end
def edit end
def update if @document.update(document_params) redirect_to @project, notice: "Document updated." else render :edit, status: :unprocessable_entity end end
private
def set_project @project = Project.find(params[:project_id]) end
def set_document @document = @project.documents.find(params[:id]) end
def document_params params.require(:document).permit(:title, :body, :status) end end`
Enter fullscreen mode
Exit fullscreen mode
The line that matters most:
params.require(:document).permit(:title, :body, :status)
Enter fullscreen mode
Exit fullscreen mode
If the user submits extra fields, Rails ignores them. That prevents mass-assignment bugs like someone trying to set user_id, admin, or other attributes you never meant to expose.
Build the form
Create a partial so both new and edit can share it.
app/views/documents/form.html.erb
<%= form_with model: [@project, @document] do |form| %> <% if @document.errors.any? %><%= form_with model: [@project, @document] do |form| %> <% if @document.errors.any? %><%= pluralize(@document.errors.count, "error") %> prevented this document from saving:
<% @document.errors.full_messages.each do |message| %> <%= message %> <% end %>
<% end %>
<%= form.label :title %> <%= form.text_field :title %>
<%= form.label :body %> <%= form.text_area :body, rows: 10 %>
<%= form.label :status %> <%= form.select :status, [["Draft", "draft"], ["Published", "published"], ["Archived", "archived"]] %>
<%= form.submit %>
<% end %>`
Enter fullscreen mode
Exit fullscreen mode
Then render it from new.html.erb:
New Document <%= render "form" %>New Document <%= render "form" %>Enter fullscreen mode
Exit fullscreen mode
And from edit.html.erb:
Edit Document <%= render "form" %>Edit Document <%= render "form" %>Enter fullscreen mode
Exit fullscreen mode
Now when validation fails, Rails re-renders the form, keeps the submitted values, and shows the error messages.
Why unprocessable_entity matters
When validation fails, don’t redirect. Render the form again with status 422.
render :new, status: :unprocessable_entity
Enter fullscreen mode
Exit fullscreen mode
That keeps the in-memory object and its errors. If you redirect, you lose the errors and the user has to start over.
Add a custom validation
AI apps often need domain-specific rules. For example, maybe you don’t want documents that are too large for your current embedding pipeline.
app/models/document.rb
class Document < ApplicationRecord belongs_to :projectclass Document < ApplicationRecord belongs_to :projectvalidates :title, presence: true, length: { maximum: 120 } validates :body, presence: true, length: { minimum: 20 } validates :status, presence: true, inclusion: { in: %w[draft published archived] } validate :body_not_too_large
private
def body_not_too_large return if body.blank? return if body.length <= 10_000
errors.add(:body, "is too large for inline processing") end end`
Enter fullscreen mode
Exit fullscreen mode
That’s a real pattern. Put business rules in the model when they protect the integrity of the record.
A common mistake: trusting the form too much
Never rely on the HTML form alone.
This is not enough:
<%= form.select :status, [["Draft", "draft"], ["Published", "published"]] %>
Enter fullscreen mode
Exit fullscreen mode
Why? Because users can still send custom HTTP requests. The browser UI is not a security boundary. The model validation is.
What to practice next
Try these changes:
-
Add validates :title, uniqueness: { scope: :project_id }
-
Add a checkbox field like featured:boolean
-
Permit the new field in document_params
-
Add a custom validation that blocks banned words in title
Once forms and validations click, you’re ready for authentication. That’s where you stop building anonymous demos and start building real applications with users, sessions, and access control.
Sign in to highlight and annotate this article

Conversation starters
Daily AI Digest
Get the top 5 AI stories delivered to your inbox every morning.
More about
modelupdateapplication
Show HN: AI tool to merge people from two photos into one realistic group photo
At a friend's gathering, someone mentioned wanting to add her late father into a family photo. I figured this would be trivial — modern image models are powerful, just send two photos and ask the AI to merge them. She said she'd tried, but the results were weird and unstable, and she didn't know how to describe what she wanted. I went home and tried it myself. With a well-written prompt and two good photos, it works. But real-world use cases aren't two good photos — it's a modern family photo plus a damaged old portrait, or two old photos from different decades. That's when things fall apart. I looked at existing tools. Most showcase merges between clean, well-lit, modern photos. Nobody was solving the hard version: mismatched eras, damaged sources, different poses, different formality lev

I Built an AI Agent That Watches the Market While I Sleep
I have a full-time job and no time to watch the stock market all day. But I still trade — mostly US tech stocks. Last year I made at least three bad decisions because I was too tired or too rushed to think clearly. So I built an AI agent to do the watching for me. The stack: OpenClaw as the agent framework, Exa for information gathering, and Milvus as a personal memory store. Total cost: about $20/month. The NVIDIA Moment On February 26th, NVIDIA reported Q4 earnings — revenue up 65% year-over-year. The stock dropped 5.5%. I didn't find out until the next morning. But when I checked my phone, there was already a message from my agent, sent the previous evening: NVDA earnings analysis: Revenue beat expectations, but the market is skeptical about AI capex sustainability. In similar past situ
Knowledge Map
Connected Articles — Knowledge Graph
This article is connected to other articles through shared AI topics and tags.
More in Products

Full-Stack E-Commerce App - Part 1: Project setup
Hey! Welcome to Part 1 of this series, where we build a complete, production-ready e-commerce app called ShopFlow — from an empty folder all the way to a live site on AWS. By the end of this series, ShopFlow will have: User authentication with JWT tokens A product catalogue with search powered by Elasticsearch A shopping cart (stored in Redis) and a full order system AI features — smart search, a chatbot, and product descriptions generated by AI Real payments via Stripe and PayPal Event-driven order processing with Apache Kafka Deployed on AWS with Kubernetes and a CI/CD pipeline That sounds like a lot — and it is! But we are going to build it one piece at a time . Each part of this series focuses on one thing, explains why we are doing it, and by the end, you have working code. In this fi

Ofcom Pushes Tech Firms to Strengthen Online Safety
More than 70 risk assessments have been legally mandated from 40 of the largest and riskiest sites and apps across the globe. Ofcom has suggested that these assessments are a crucial part of keeping users safe online, and act as guides to putting appropriate safety measures in place. The guardrails are supposed to keep all [ ] The post Ofcom Pushes Tech Firms to Strengthen Online Safety appeared first on DIGIT .




Discussion
Sign in to join the discussion
No comments yet — be the first to share your thoughts!