'Namespacing service objects in Rails 6 with Zeitwerk autoloader

Rails 6 switched to Zeitwerk as the default autoloader. Zeitwerk will load all files in the /app folder, eliminating the need for namespacing. That means, a TestService service object in app/services/demo/test_service.rb can now be directly called e.g. TestService.new().call.

However, namespacing has been helpful to organize objects in more complex rails apps, e.g. API::UsersController, or for services we use Registration::CreateAccount, Registration::AddDemoData etc.

One solution suggested by the rails guide is to remove the path from the autoloader path in application.rb, e.g. config.autoload_paths -= Dir["#{config.root}/app/services/demo/"]. However, that feels like a monkey patch for shoehorning an old way or organizing objects into the new rails way.

What is the correct way of namespacing objects or a rails 6 way of organizing it without just forcing rails into the old way?



Solution 1:[1]

It is not true to say that Zeitwerk eliminates 'the need for namespacing'. Zeitwerk does indeed autoload all the subdirectories of app (except assets, javascripts, and views). Any directories under app are loaded into the 'root' namespace. But, Zeitwerk also 'autovivifies' modules for any directories under those roots. So:

/models/foo.rb => Foo
/services/bar.rb => Bar
/services/registration/add_demo_data.rb => Registration::AddDemoData

If you are already used to loading constants from 'non-standard' directories (by adding to config.autoload_paths), there's usually not much change. There are a couple of cases that do require a bit of tweaking, though. The first is where you are migrating a project that just adds app itself to the autoload path. In classic (pre-Rails 6), this allows you to use app/api/base.rb to contain API::Base, whereas in Zeitwerk it would expect it to contain only Base. That's the case you mention above where the recommendation is to exclude that directory from the autoload path. Another alternative would be to simply add a wrapper directory like app/api/api/base.rb.

The second issue to note is how Zeitwerk infers constants from file names. From the Rails migration guide:

classic mode infers file names from missing constant names (underscore), whereas zeitwerk mode infers constant names from file names (camelize). These helpers are not always inverse of each other, in particular if acronyms are involved. For instance, "FOO".underscore is "foo", but "foo".camelize is "Foo", not "FOO".

So, /api/api/base.rb actually equates to Api::Base in Zeitwerk, not API::Base.

Zeitwerk includes a rake task to verify autoloading in a project:

% bin/rails zeitwerk:check
Hold on, I am eager loading the application.
expected file app/api/base.rb to define constant Base

Solution 2:[2]

I'm posting separate answer, but actually accepted answer has all the good information. Since my comment was bigger than allowed, I chose to add separate answer for those who are struggling with similar issue.

We have created "components" under app where we separate domain specific namespaces/packages. They co-exists with some "non-component" Rails parts, that are hard to move under components. With classic autoloader, we have added #{config.root}/app in our autoload_paths.

This setup fails for Zeitwerk and removing "#{config.root}/app" from autoload_paths didn't help. rmlockerd suggestion to move app/api/ under /app/api/api moved me thinking in creating separate 'app/components' and moving all components under this directory and add this path to autoload_paths. Zeitwerk likes this.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2