Just got back from RailsConf. It was a great Ruby & Rails conference, but I was struck by the dearth of talks about new features of Rails 5—because there just aren’t many. I thought back to what excited me about Rails in the first place—the baked-in conventions, convenience, encouragement of good practices.
Justin Searls gave a talk not about RSpec but about how Rails is losing mindshare, losing favor. Is Rails losing relevance? I hope not, but it’s easy to see how someone less emotionally invested than me might see it that way.
I’m trying to write about this without complaining. This is my fifth attempt. It’s hard not to just rant about Rails’ failings, so I’m going to try to be constructive by outline a fantasy roadmap for Rails 6.
I tried very much to think about this without going against the “Rails Doctrine”, and have written this as if it were an Apple-style keynote. The theme is Progress.
Rails 6: Progress
Good morning. I’d like to share with you all the exciting changes we have in store for Rails 6. Since Rails was first launched 11 years ago, a lot has changed in the world of web development.
Billion-dollar business are being run on Rails. These business—along with countless others—have been able to use Rails to solve problems we never imagined. Rails not only enables small teams to ship great software, but it works for big teams, too.
We’ve seen Rails move beyond its role as “the single application” to being a part of highly distributed systems. The productivity gains developers get in the small, and in the simple, they get in the large and complex, too.
Meanwhile, there have been tremendous strides in what is possible in a web browser. Rails was born of a love of the web, and the web has gotten so much better over the years. The front-end has blossomed. We can now deliver amazing things to our users, and the community of web developers both inside and outside Rails has produced amazing tools and techniques for doing so.
Finally, the community of Rails and Ruby developers have over a decade of experience using Rails. A decade! We’ve learned so much about what it’s like to maintain Rails applications over long periods, and how to best use developers of all levels of experience in working on Rails applications.
Rails 6 is the most revolutionary release of Rails ever, and we’ve rallied around seventh pillar of the Rails Doctrine: Progress.
Let’s start with the front-end.
Sprockets was way ahead of its time. Using
remote=true was, too. Rails was one of the first web frameworks to actually acknowledge the front-end and include powerful tools for managing your assets.
The rest of the industry has not embraced these tools or techniques, but has instead solved the problems of front-end development in other ways. Although the tools used for front-end development are in constant flux, there are stable, mature techniques that have proven results.
Rails 6 will bake in the best of these techniques, and create an updated, modern set of front-end tools.
In Rails 6:
- No more per-view CSS. Rails 6 encourages OOCSS and ships with Tachyons, allowing developers to style their views without any CSS from day 1.
- Rails 6 no longer includes jQuery by default. Instead, Rails includes a library called
rails.jsthat provides a simple and standard way to access Rails resources via AJAX without using jQuery. Rails.js can be used standalone, or in conjunction with existing front-end frameworks like React.
- Source maps will be generated for any front-end compiled assets or code, if the source language supports it.
- A new focus on simple plugability and transparent behavior. The one constant in front-end tooling is change, and a full-stack web application framework like Rails should give you the ability to keep up.
Next up, the controller layer.
Doubling-down on Resource-based Design.
Rails strongly believes in resource-based design. It’s been a part of Rails since Rails 2. Too often, however, developers deviate from this design when without a strong reason. We’ve realized this happens for two different reasons.
First, it’s largely the same amount of work in your routes and controller to use RPC-style routing as it is to use resourceful routing. Second, it’s too difficult to create resources that work with Rails but that aren’t ActiveRecords.
In Rails 6, developers who embrace resource-based design no longer need to specify routes explicitly. The “special 7” routes are configured automatically by declaring the needed methods in the controller.
class UsersController < ApplicationController def index # automatically sets up GET /users end def create # automatically sets up POST /users # also sets up GET /users/new end opt_out :new # remove GET /users/new def destroy # configure DELETE /users/:I'd end def deactivate # does not introduce any route end end
Developers unnecessarily using RPC-based designs now have more work to do than if they stick with a resource-based design. The old way of declaring routes still works, but we feel this method will be much simpler.
We also want to make it easier to describe resources that work with the Rails View layer but that aren’t Active Records. We introduced helper modules in Rails 3, and an explicit Active Model in Rails 4, but we still see developers struggling to adopt these technologies.
Instead, developers use presenters, decorators, and view models either hand-rolled or from one or more third-party gems.
Rails 6 will build on ActiveModel by providing a base class called ActionResource.
An ActionResource makes it easy to totally describe your resource, with support for:
- delegating to an underlying object.
- create derived fields.
- formatting values.
- interoperability with Rails form and URL helpers.
Suppose we want to display a user’s name, signup date, email, and most recent order (a date and description). We’ll call this an account. We’ll implement it using ActionResource, which we can use in our controller like so:
class AccountsController def show @account = Account.new(customer: current_user) end end
Account like so:
class Account < ActionResource from :customer, show: [ :email, :name, :created_at ] format :created_at, with: :short format :email, with :downcase def last_order_date time_ago_in_words(last_order.created_at) end def last_order_description last_order.items.count + " items" end private def last_order @last_order ||= customer.orders.last end end
As with much of Rails, how this works should be obvious by the API we’ve designed.
The resulting object can be used like any model object:
customer = Customer.new(name: "Bob Jones", email: "Bob@JONES.net") customer.orders << Order.new(created_at: 3.days.ago, items: Item.new) account = Account.new(customer: customer) account.email # firstname.lastname@example.org account.name # Bob Jones account.created_at # May 13 account.last_order_date # 3 days ago account.last_order_description # 1 items
We hope this will allow Rails developers to effectively use resourceful design without needing third-party gems, and to do so with minimal, beautiful code.
We’re also discouraging the use of helpers in Rails 6. Helpers are still a fully supported feature, but when scaffolding or creating new resources with the Rails generator, you will no longer be given an empty helper file. ActionResource can replace most need for helpers.
Next, let’s talk about the code that makes your app special: the business logic.
The Business of Logic
Over the last ten years, it’s become clear that every Rails application needs to manage code that doesn’t belong to a controller, model, job, or mailer. While there are many opinions on exactly how much code should go where, there’s no debate that sometimes code has to go somewhere else, and Rails hasn’t had an opinion. Until now.
Rails 6 supports services, as we felt this was a good enough nudge regarding where code could go, but without being overly prescriptive, or requiring developers to opt-in to a complex DSL.
Make now mistake: this is a radical change in how Rails thinks about your application’s architecture.
app/services will be created when you run
rails new and any code you place there will be auto-loaded the same as for your controllers or models. The code in your services can be anything.
However, when writing good services, it’s often required to bring together lower-level libraries, third-party code, and other services to get the job done. Testing this code can be challenging.
To help organize your services, we’re also introducing ActiveService, which is a lightweight library that makes writing and testing services as easy as it can be.
Suppose you want a service for charging customers some money using your
Order models. To do this, we need to locate the customer’s credit card, charge it the amount of their order, and then email them a receipt (or a notification about failure). To do this, we need access to our payment processor’s Ruby library as well as our
OrderMailer Rails mailer.
While you could access these classes in the normal way of referencing their global symbols, we can use less code that’s easier to test by using Active Service.
class Purchaser < ActiveService::Base needs :payment_transaction needs :credit_cards needs :order_mailer def purchase!(customer:, order:) card = credit_cards.find(customer.id) transaction = payment_transaction.new(card,order.amount) if transaction.success? order_mailer.receipt_email(customer,order).deliver_later else order_mailer.card_failed(customer,order,transaction).deliver_later end end end
As you can see, ActiveService provides a way to manage all your services at runtime. This is especially useful for third-party libraries like our payment processor. Instead of configuring a global constant like
BRAINTREE_TRANSACTION, we can let ActiveService handle it:
# config/initializers/braintree.rb service :payment_transaction do Braintree::Transaction.new(api_key: ENV["BRAINTREE_API_KEY"]) end service :credit_cards do Braintree::PaymentCard.new(api_key: ENV["BRAINTREE_API_KEY"]) end
This way, any other service that needs access to this can be sure to get the properly configured objects. The real benefit, however, is in our tests.
Instead of mocking classes, or using code like
allow_any_instance_of, Rails takes care of all that. Since Rails now knows what your services needs to do its job, it can intelligently mock the behavior of
those objects and allow you to test your code in isolation.
For example, to test our
purchase! method, we need to arrange for
payment_transaction to return a successful result, and check that the right mailer method was called.
class PurchaserTest < ActiveService::Test::Base def test_purchase_succeeded successful_transaction = stub(success?: true) order = orders(:any) # we'll talk about these customer = customers(:any) # later on in the presentation purchaser.payment_transaction.on(:new).returns(successful_transaction) purchaser.purchase!(customer, order) purchaser.order_mailer.verify(:receipt_email).with(customer,order) end end
No matter how much code you write as services, Rails 6 will make it easy to keep your code clean, simple, and tested.
We’d like to talk about the fixture-like code above, but first we need to learn about how Rails 6 treats your database.
Rails and Databases: It’s Complicated
One thing we’ve come to understand over the years is that Rails is just a piece of your technical architecture. The days of one company having exactly one Rails app that controls one database are increasingly rare. What we’ve also seen is that in most cases, the data is more important than code. Many companies use Rails to create applications that, at their core, manage the data in a database. This is very much what Rails was originally designed to do!
Rails 6 is now much more opinionated about how you should manage your data and interact with your database. Instead of a “common denominator” approach, Rails 6 is designed to get the most out of your data store—whichever brand it might be.
To state this another way, we want good database design to be easy but bad design to be possible.
In Rails 6, we’re making the following changes:
- Columns are no longer nullable by default.
- Numeric-based enums are deprecated. If your database supports enums, those are used, otherwise string-based enums are the default.
- Foreign keys have constraints by default, if your database supports it.
- Primary keys are UUIDs by default, if your database supports it.
- There is a new DSL for check constraints, if supported by your database, that also create equivalent ActiveRecord validations in the related model.
These new defaults and features will keep the migrations DSL just as easy to use as it’s ever been, but result in a consistent, resilient, well-designed database that works great with Active Record.
Making this happen isn’t easy, and the biggest trickle-down affect it had is the way we manage test data.
Test data comes in two flavors: reference data that is mostly immutable throughout an app’s lifetime (think: country codes), and transactional data, which is the data the app exists to manage (for example, orders).
Reference data can be managed with fixtures, however transactional data will now be managed with factories.
It’s too difficult to manage one single set of test data that works for every test case. Instead, each use-case for your data will be described by a factory. You can create a global repository of factories—similar to what you might do with FactoryGirl—or you can create factories directly in your test cases.
By default, however, objects created with factories won’t be written to the database. This will make your tests run much faster, because you typically just need to test logic—not Active Record.
One problem with this approach, however, is that it’s possible to create use-case-based factories that could never happen in production. This is why Rails 6 includes a standard linting task that will write all your factories to the database, to ensure that your scenarios actually could exist in the real world.
We also have a few more changes in how to write tests for Rails apps.
In Rails 6, there will now be only three types of tests.
Unit Tests do not use the database by default, and are where the majority of your tests should be written. The code tested here would be any of your business logic in your models or services. These are in
For testing scopes, or other code that runs database queries, Rails 6 supports active record tests. We expect these to be infrequent, but it’s difficult to test database queries without executing against real data. These live in
Finally, Rails 6 integration test support will be based on end-to-end in-browser testing using PhantomJS. You’ll never have confidence in your web application until you’ve used it in a real web browser. We realize these types of tests can be slow, but we’ve rarely seen a Rails app that doesn’t need them. They will live in
We hope you’re as excited for Rails 6 as we are. This is going to be the best release of Rails yet.
I know this is a fantasy. I also know that the amount of work outlined here is massive. But I strongly feel that features like the ones I’ve made up would address common things faced by all Rails developers. Heck, these are issues faced by any web application developer, and Ruby is one of the few languages where solutions can be easily provided out of the box.
Maybe my particular solutions aren’t the best, and there are certainly many issues my hand-wavy features aren’t addressing that might make them really difficult to actually implement.
But, wouldn’t it be amazing if Rails 6 shipped with features kinda like this? Wouldn’t it be awesome if Rails 6 acknowledged the community’s collected learnings over the last 10 years, and acknowledged that Rails is actually really awesome at things other than what a small team can accomplish? Wouldn’t be great if Rails acknowledged advances made outside the Rails community?
I do wonder if this is even possible. Can Rails make such fundamental changes any longer? Even if we set aside the technical challenges—which are huge—could the culture of Rails (and its maintainers) allow these things to happen? Should they?
Let’s say it did. Let’s say Rails 6 was the revolutionary release I’m pretending it is. Would that put Rails back in the spotlight? Would that make Rails the go-to web framework for web development? Would CTOs start thinking about Rails again?
I don’t know.