Introduction
Rails 7.1 didn’t ship one big flashy feature. It shipped a pile of small, practical ones — the kind that quietly delete boilerplate you’ve been writing by hand for years.
These are the ones I actually reach for.
normalizes: clean attributes without a callback
How many times have you written a before_save just to downcase an email?
Rails 7.1 turns that into a declaration:
class User < ApplicationRecord
normalizes :email, with: ->(email) { email.strip.downcase }
end
The normalization runs on assignment, so user.email = " Me@Example.com "
stores me@example.com. Better still, it also applies to finders:
User.find_by(email: " Me@Example.com ")
# looks up "me@example.com"
No more remembering to normalize in three places. The rule lives in one.
generates_token_for: scoped, self-invalidating tokens
Password resets, email confirmations, unsubscribe links — they all need a signed token that’s tied to a purpose and expires. Rails 7.1 bakes that in:
class User < ApplicationRecord
has_secure_password
generates_token_for :password_reset, expires_in: 15.minutes do
password_salt&.last(10)
end
end
token = user.generate_token_for(:password_reset)
# Later, in the controller:
User.find_by_token_for(:password_reset, token) # => the user, or nil
The clever part is the block. Whatever it returns is embedded in the token and checked on lookup, so when the user’s password changes their salt changes, and every outstanding reset token becomes invalid automatically. No extra column, no manual expiry bookkeeping.
authenticate_by: login without the timing leak
If you authenticate with has_secure_password, the obvious code has a subtle
flaw:
user = User.find_by(email: params[:email])
user&.authenticate(params[:password])
When no user matches, find_by returns immediately and skips the password
digest — so a login attempt for a non-existent account is measurably faster.
That timing difference leaks which emails exist. authenticate_by closes it:
User.authenticate_by(email: params[:email], password: params[:password])
# => the user on success, nil otherwise — in constant time
It always does the digest work, matching or not, so you can’t tell a missing account from a wrong password by timing it.
Composite primary keys
Rails 7.1 supports composite primary keys as a first-class concept, derived
from the schema. Join tables and models keyed on more than one column
([:tenant_id, :id]) no longer need a workaround gem — associations, find,
and query building understand the composite key directly.
Async aggregates
For pages that fire several independent queries, 7.1 adds asynchronous
aggregate methods: async_count, async_sum, async_average,
async_minimum, async_maximum, async_pluck, async_pick, async_ids.
Each returns a promise you resolve later, so the queries run concurrently instead of one after another:
total = Order.async_count
revenue = Order.async_sum(:amount)
# both queries are already in flight
[total.value, revenue.value]
It only pays off with a database and adapter that support concurrent connections, but for a heavy dashboard it’s real wall-clock time saved.
A few smaller wins
perform_all_laterenqueues many jobs in one call, using bulk-enqueue on adapters that support it instead of a round trip per job.- Dockerfiles by default —
rails newnow generates a productionDockerfile,.dockerignoreand entrypoint, so containerised deploys start from a sane baseline. config.autoload_lib(ignore: %w[assets tasks])finally makesliba proper autoload path, without hand-rolling the configuration.
Wrapping up
None of this is glamorous, and that’s rather the point. Rails 7.1 is a
maintenance-and-polish release: normalizes and generates_token_for alone
delete a callback and a whole token model I used to write by hand, and
authenticate_by fixes a security footgun most of us had in our login code
without realising it. Unglamorous, but I use these every week.
Have comments or want to discuss this topic?
Send an email to ~bounga/public-inbox@lists.sr.ht