George Claghorn

Namespacing GraphQL constants in Rails apps

Update: I’ve extracted the configuration described in this post to a Rails plugin named graphql-rails-namespace. You should use the plugin instead, but I’ll leave this post up for posterity.

The GraphQL Ruby library encourages placing GraphQL modules and classes such as type definitions under the app/graphql/ directory in Rails applications. Here’s an example from the GraphQL Ruby homepage:

# app/graphql/types/profile_type.rb
class Types::ProfileType < Types::BaseObject
  field :id, ID, null: false
  field :name, String, null: false
  field :avatar, Types::PhotoType
end

Because Rails treats all subdirectories of app as root directories for the purpose of autoloading, classes in app/graphql/types/ are nested in a top-level Types module. In the above example, app/graphql/types/profile_type.rb defines Types::ProfileType. But Types is too general a top-level namespace for GraphQL type definitions. The GraphQL schema class defined in app/graphql/schema.rb is named Schema, but it’s specifically a GraphQL schema, not a generic schema. It would be nice to nest GraphQL-specific modules and classes in a GraphQL-specific namespace.

The most obvious way to achieve this is to nest the files in an app/graphql subdirectory—for example, app/graphql/application_graph/. Then we can nest GraphQL constants in the corresponding module (ApplicationGraph::Types::ProfileType and ApplicationGraph::Schema).

With some custom configuration, it’s possible to avoid the additional directory. First, in config/application.rb, remove app/graphql from the list of root directories:

module Foo
  class Application < Rails::Application
    # ...
    config.eager_load_paths -= [ "#{Rails.root}/app/graphql" ]
  end
end

This must be done in config/application.rb and not an initializer because Rails freezes config.eager_load_paths before initializers run.

Next, define the top-level ApplicationGraph module. (Use a different name if you prefer, but I recommend avoiding GraphQL since GraphQL Ruby already uses it.) Configure the autoloader to use ApplicationGraph to namespace constants defined in app/graphql/. At the end of config/application.rb, after the application definition:

module ApplicationGraph; end
Rails.autoloaders.main.push_dir Rails.root.join("app/graphql"), namespace: ApplicationGraph

This can be placed in an initializer instead, but it’s nice to keep all the GraphQL autoloading config together.

Finally, because we removed app/graphql/ from config.eager_load_paths, Rails doesn’t know to reload when Ruby files in that directory are changed. Tell it to do so:

# config/application.rb
module Foo
  class Application < Rails::Application
    # ...
    config.watchable_dirs["#{Rails.root}/app/graphql"] = %w[ .rb ]
  end
end

All together, the changes look like this:

# config/application.rb
module Foo
  class Application < Rails::Application
    # ...

    config.eager_load_paths -= [ "#{Rails.root}/app/graphql" ]
    config.watchable_dirs["#{Rails.root}/app/graphql"] = %w[ .rb ]
  end
end

module ApplicationGraph; end
Rails.autoloaders.main.push_dir Rails.root.join("app/graphql"), namespace: ApplicationGraph

Now all files in app/graphql/ define classes under the top-level ApplicationGraph namespace: app/graphql/schema.rb defines ApplicationGraph::Schema, app/graphql/types/profile_type.rb defines ApplicationGraph::Types::ProfileType.