Active Record Validations & Callbacks
✅ Active Record Validations & Callbacks
create and update actions branched on if @post.save without fully explaining why .save could ever return false. This chapter answers that: validations live directly on the model, run automatically before every save, and replace the manual validation logic Express handles in middleware or a service layer (Chapter 1's reviewed example).
✔️ validates — Declaring Rules on the Model
Common built-in validators: presence (not blank), length (min/max), uniqueness (no duplicate value across all rows), numericality (must be a number, optionally within a range), and format (must match a regex). Every one of these runs automatically before .save actually writes to the database.
❓ How Validations Actually Work
This is exactly the mechanism behind Chapter 3's if @post.save ... else render :new ... end pattern: an invalid record makes .save return false, and post.errors holds a real, structured collection of what went wrong — which is what a re-rendered form (Chapter 8) uses to show error messages next to the offending fields.
🏢 Model-Level vs Middleware-Level Validation
The Express example reviewed back in Chapter 1 hand-wrote its validation inside a service function — if (!data?.name?.trim()) errors.push(...) — which only runs if that specific service function is actually called. Any other code path that touches the database directly (a script, a different service, a background job) bypasses it entirely, since the validation isn't attached to the data itself. Rails' validates lives directly on the Post model, so any code that calls .save on a Post — a controller, the Rails console, a background job — automatically runs the same rules, with no risk of one entry point forgetting to re-implement them.
🔧 Custom Validations
When a rule doesn't fit a built-in validator, validate :method_name (singular) runs your own instance method, which manually calls errors.add if something's wrong — the equivalent of writing an arbitrary custom check inside an Express middleware or service function, just triggered by the same automatic save lifecycle as the built-in validators.
🔄 Callbacks — Lifecycle Hooks
Callbacks run automatically at specific points in a record's save/create/destroy lifecycle — before_save before any write, before_create only on the first insert, after_create after a new row exists, and similarly-named hooks for updates and destruction. This is comparable to Express middleware running before/after a route handler, except scoped to the model's own lifecycle rather than the HTTP request lifecycle.
notify_subscribers above means calling post.save anywhere in the codebase silently sends an email — a real side effect, triggered from a place that, reading just the calling code, looks like nothing more than "save this record." In Express, that same side effect would be an explicit, visible line inside the route handler or service function. Overusing callbacks for anything beyond simple, self-contained data preparation (like normalize_title above) is a well-known Rails anti-pattern precisely because it hides consequential side effects behind an innocent-looking method call.
📜 Validation & Side Effects, Side by Side
Express vs Rails
| Concern | Express | Rails |
|---|---|---|
| Where validation lives | Middleware or service-layer code, per route | Directly on the model, via validates |
| Applies to every code path? | Only if every entry point calls the same validation | Yes — automatic on every .save, everywhere |
| Reporting what went wrong | Manually built error array/object | record.errors, structured and built in |
| Side effects around a save | Explicit lines in the route/service | Callbacks — implicit, can be hard to trace |
💻 Coding Challenges
Challenge 1: Validate a Model
Add validations to a Product model: name must be present with a maximum length of 50, price must be a number greater than 0, and sku must be unique. Then write code that builds an invalid Product (blank name, negative price), calls .valid?, and prints errors.full_messages.
Goal: Practice the most common built-in validators together on one model.
Challenge 2: A Custom Validation
Write a custom validation on an Event model ensuring end_time is always after start_time — a rule no single built-in validator can express — using validate :method_name and errors.add.
Goal: Practice writing a validation for a business rule that spans two fields.
Challenge 3: A Careful Callback
Write a before_save :downcase_email callback on a User model that normalizes the email attribute to lowercase before every save. Then write two sentences: one explaining why this particular callback is safe/appropriate, and one explaining what would make a different callback (like sending a welcome email) risky by comparison.
Goal: Practice writing an appropriately-scoped callback and articulate the line between safe and risky callback use.
The real win this chapter is about centralization, not brevity: a validation rule written once on a model applies everywhere that model is ever saved, for the entire lifetime of the application — no risk of a new controller action, a background job, or a console script quietly bypassing a rule that only lived in one specific Express route's middleware. That guarantee is worth more than the line count it saves.
🎯 What's Next
Next chapter — the final chapter of this course: Forms & Strong Parameters — form_with, params.require().permit(), and Rails' built-in CSRF protection, contrasted with the manual work Express needs for the same protections.