Routing Module
Plutonium's routing system transforms the way you think about Rails routing. Instead of manually defining dozens of routes, you simply register your resources and Plutonium automatically generates comprehensive routing structures including CRUD operations, nested associations, interactive actions, and multi-tenant scoping.
TIP
The Routing module is located in lib/plutonium/routing/ and seamlessly extends Rails' built-in routing system.
The Routing Revolution
Traditional Rails routing requires you to manually define every route, leading to repetitive, error-prone route files. Plutonium's approach is radically different:
Traditional Rails Approach:
# Lots of manual route definition
resources :posts do
member do
post :publish
post :archive
end
resources :comments, except: [:new, :edit]
end
resources :users do
resources :posts, controller: 'users/posts'
resources :comments, controller: 'users/comments'
endPlutonium Approach:
# Simple, declarative registration
register_resource Post
register_resource Comment
register_resource User
# Plutonium automatically generates:
# - All CRUD routes
# - Nested association routes
# - Interactive action routes
# - Multi-tenant scoped routesCore Routing Principles
Plutonium's routing system is built on four fundamental concepts:
- Declarative Registration: Register resources instead of defining individual routes
- Intelligent Generation: Routes are created based on your model associations and definitions
- Entity Scoping: Automatic multi-tenant routing with parameter injection
- Interactive Actions: Dynamic routes for business operations and user interactions
Resource Registration: The Foundation
Basic Resource Registration
The heart of Plutonium routing is the register_resource method:
# packages/admin_portal/config/routes.rb
AdminPortal::Engine.routes.draw do
root to: "dashboard#index"
# Register your resources - that's it!
register_resource User
register_resource Post
register_resource Comment
endWhat Registration Creates
When you register a single resource, Plutonium automatically generates:
register_resource Post
# Standard CRUD routes:
# GET /posts # index - list all posts
# GET /posts/new # new - form for creating posts
# POST /posts # create - handle post creation
# GET /posts/:id # show - display specific post
# GET /posts/:id/edit # edit - form for editing posts
# PATCH /posts/:id # update - handle post updates
# PUT /posts/:id # update - alternative update method
# DELETE /posts/:id # destroy - delete posts
# Interactive action routes:
# GET /posts/resource_actions/:action # Resource-level operations
# POST /posts/resource_actions/:action # Execute resource operations
# GET /posts/:id/record_actions/:action # Individual record operations
# POST /posts/:id/record_actions/:action # Execute record operations
# GET /posts/bulk_actions/:action # Bulk operations on multiple records
# POST /posts/bulk_actions/:action # Execute bulk operations
# Nested association routes (if Post has_many :comments):
# GET /posts/:post_id/nested_comments # Comments belonging to a post
# GET /posts/:post_id/nested_comments/:id # Specific comment in contextAdvanced Registration Options
Singular Resources
For resources that don't need collection routes:
register_resource Profile, singular: true
# Generates singular routes:
# GET /profile # show
# GET /profile/new # new
# POST /profile # create
# GET /profile/edit # edit
# PATCH /profile # update
# DELETE /profile # destroyCustom Routes with Blocks
Add custom routes alongside the standard ones:
register_resource Post do
# Member routes (operate on specific posts)
member do
get :publish # GET /posts/1/publish
post :archive # POST /posts/1/archive
patch :featured # PATCH /posts/1/featured
end
# Collection routes (operate on post collection)
collection do
get :search # GET /posts/search
get :recent # GET /posts/recent
post :bulk_update # POST /posts/bulk_update
end
# Nested resources for complex relationships
resources :comments, only: [:index, :show]
# Alternative syntax for single routes
get :preview, on: :member # GET /posts/1/preview
endHandling Custom Routes in Controllers:
class PostsController < ApplicationController
include Plutonium::Resource::Controller
# Custom member actions
def publish
authorize_current!(resource_record!)
resource_record!.update!(published: true)
redirect_to resource_url_for(resource_record!), success: "Post published!"
end
def archive
authorize_current!(resource_record!)
resource_record!.update!(archived: true)
redirect_to resource_url_for(resource_class), success: "Post archived!"
end
# Custom collection actions
def search
authorize_current!(resource_class)
@query = params[:q]
@posts = resource_scope.where("title ILIKE ?", "%#{@query}%")
render :index
end
endAutomatic Nested Resource Generation
One of Plutonium's most powerful features is automatic nested route generation based on your ActiveRecord associations.
How Association-Based Routing Works
# Define your model associations
class User < ApplicationRecord
include Plutonium::Resource::Record
has_many :posts
has_many :comments
has_many :projects
end
class Post < ApplicationRecord
include Plutonium::Resource::Record
belongs_to :user
has_many :comments
end
# Register resources normally
AdminPortal::Engine.routes.draw do
register_resource User
register_resource Post
register_resource Comment
endPlutonium automatically generates nested routes:
# User's nested resources:
# GET /users/:user_id/nested_posts # User's posts
# GET /users/:user_id/nested_posts/:id # Specific post by user
# GET /users/:user_id/nested_comments # User's comments
# GET /users/:user_id/nested_projects # User's projects
# Post's nested resources:
# GET /posts/:post_id/nested_comments # Post's comments
# GET /posts/:post_id/nested_comments/:id # Specific comment on postNested Route Naming Convention
Nested routes use the nested_#{resource_name} pattern to avoid conflicts:
- Standard route:
/posts→PostsController#index - Nested route:
/users/:user_id/nested_posts→PostsController#index(withcurrent_parent)
Automatic Parent Resolution
Controllers automatically handle parent relationships in nested contexts:
class PostsController < ApplicationController
include Plutonium::Resource::Controller
def index
# When accessed via /users/123/nested_posts
current_parent # => User.find(123) - automatically resolved
parent_route_param # => :user_id
parent_input_param # => :user (the belongs_to association name)
# Parameters are automatically merged for creation
resource_params # => includes user: current_parent
# URLs automatically include parent context
resource_url_for(Post) # => "/users/123/nested_posts"
resource_url_for(@post) # => "/users/123/nested_posts/456"
end
endEntity Scoping: Multi-Tenant Routing
Entity scoping automatically transforms your routes to support multi-tenancy, where all data is scoped to a parent entity like Organization or Account.
Path-Based Scoping
The most common approach uses URL path parameters:
# Engine configuration
class AdminPortal::Engine < Rails::Engine
include Plutonium::Portal::Engine
scope_to_entity Organization, strategy: :path
endRoute Transformation:
# Without scoping:
# GET /posts
# GET /posts/:id
# With path scoping:
# GET /:organization_id/posts
# GET /:organization_id/posts/:idCustom Scoping Strategies
For more sophisticated multi-tenancy patterns:
# Subdomain-based scoping
scope_to_entity Organization, strategy: :current_organization
# Custom parameter key
scope_to_entity Organization,
strategy: :path,
param_key: :org_slug
# Routes become: GET /:org_slug/postsRequired Controller Implementation:
module AdminPortal::Concerns::Controller
private
# Method name MUST match the strategy name exactly
def current_organization
@current_organization ||= Organization.find_by!(subdomain: request.subdomain)
rescue ActiveRecord::RecordNotFound
redirect_to root_path, error: "Invalid organization"
end
endEntity Scoping with Nested Routes
Scoping applies to both standard and nested routes:
scope_to_entity Organization, strategy: :path
# Standard scoped routes:
# GET /:organization_id/users
# GET /:organization_id/posts
# Nested scoped routes:
# GET /:organization_id/users/:user_id/nested_posts
# GET /:organization_id/posts/:post_id/nested_commentsSmart URL Generation
Plutonium provides intelligent URL generation that handles scoping, nesting, and context automatically.
The resource_url_for Method
This is your go-to method for generating resource URLs:
# Basic usage
resource_url_for(User) # => "/users"
resource_url_for(@user) # => "/users/123"
resource_url_for(@user, action: :edit) # => "/users/123/edit"
# With entity scoping
resource_url_for(@user) # => "/organizations/456/users/123"
# Nested resources
resource_url_for(Post, parent: @user) # => "/users/123/nested_posts"
resource_url_for(@post, parent: @user) # => "/users/123/nested_posts/789"
# Override parent context
resource_url_for(@post, parent: nil) # => "/posts/789"
# Different actions
resource_url_for(@post, action: :edit, parent: @user)
# => "/users/123/nested_posts/789/edit"Interactive Action URLs
Special URL generation for interactive actions:
# Record-level actions (operate on specific records)
record_action_url(@post, :publish)
# => "/posts/123/record_actions/publish"
# Resource-level actions (operate on the resource class)
resource_action_url(Post, :import)
# => "/posts/resource_actions/import"
# Bulk actions (operate on multiple records)
bulk_action_url(Post, :archive, ids: [1, 2, 3])
# => "/posts/bulk_actions/archive?ids[]=1&ids[]=2&ids[]=3"Dynamic URL Generation for Actions
For actions that need context-aware URL generation, use RouteOptions with custom url_resolver:
# In a resource definition
class ProjectDefinition < Plutonium::Resource::Definition
# Dynamic parent-child navigation
action :create_deployment,
label: "Create Deployment",
icon: Phlex::TablerIcons::Rocket,
record_action: true,
route_options: Plutonium::Action::RouteOptions.new(
url_resolver: ->(subject) {
resource_url_for(UniversalFlow::Deployment, action: :new, parent: subject)
}
)
# Conditional routing based on permissions
action :manage_settings,
label: "Settings",
resource_action: true,
route_options: Plutonium::Action::RouteOptions.new(
url_resolver: ->(subject) {
if current_user.admin?
admin_project_settings_path(subject)
else
project_settings_path(subject)
end
}
)
# External system integration
action :view_in_external_system,
label: "View Externally",
record_action: true,
route_options: Plutonium::Action::RouteOptions.new(
url_resolver: ->(subject) {
"https://external-system.com/projects/#{subject.external_id}"
}
)
endThe url_resolver lambda receives:
- For record actions: The current record instance
- For resource actions: The resource class
- For bulk actions: The resource class (with selected IDs available in params)
Context-Aware URL Generation
In nested controller contexts, URLs automatically include proper context:
class PostsController < ApplicationController
include Plutonium::Resource::Controller
def show
# When accessed via /users/123/nested_posts/456
# These automatically include the user context:
resource_url_for(Post) # => "/users/123/nested_posts"
resource_url_for(@post, action: :edit) # => "/users/123/nested_posts/456/edit"
# Parent is automatically detected:
current_parent # => User.find(123)
end
endAdvanced Routing Patterns
Multiple Engine Mounting
Different engines can have different routing strategies:
# config/routes.rb
Rails.application.routes.draw do
# Admin portal with organization scoping
constraints Rodauth::Rails.authenticate(:admin) do
mount AdminPortal::Engine, at: "/admin"
end
# Customer portal with account scoping
constraints Rodauth::Rails.authenticate(:customer) do
mount CustomerPortal::Engine, at: "/app"
end
# Public portal with no scoping or authentication
mount PublicPortal::Engine, at: "/"
endRoute Constraints and Conditions
Rails.application.routes.draw do
# Subdomain-based portal mounting
constraints subdomain: 'admin' do
mount AdminPortal::Engine, at: "/"
end
# Feature flag-based mounting
constraints ->(request) { FeatureFlag.enabled?(:beta_portal) } do
mount BetaPortal::Engine, at: "/beta"
end
# IP-based constraints for admin access
constraints ip: /192\.168\.1\.\d+/ do
mount AdminPortal::Engine, at: "/secure-admin"
end
endRoute Generation Lifecycle
Understanding how Plutonium generates routes helps with debugging:
1. Registration Phase:
register_resource Post
# - Resource is registered with the engine
# - Route configuration is created and stored
# - Concern name is generated (posts_routes)2. Route Definition Phase:
concern :posts_routes do
resources :posts, controller: "posts", concerns: [:interactive_resource_actions] do
# Nested routes for has_many associations
resources "nested_comments", controller: "comments"
end
end3. Route Materialization Phase:
scope :organization_id, as: :organization_id do
concerns :posts_routes, :comments_routes, :users_routes
end
# - All registered concerns are materialized within appropriate scope
# - Entity scoping parameters are applied
# - Final route table is generatedDebugging and Troubleshooting
Inspecting Generated Routes
# View all routes for an engine
AdminPortal::Engine.routes.routes.each do |route|
puts "#{route.verb.ljust(6)} #{route.path.spec}"
end
# View registered resources
AdminPortal::Engine.resource_register.resources
# => [User, Post, Comment]
# View route configurations
AdminPortal::Engine.routes.resource_route_config_lookup
# => { "posts" => {...}, "users" => {...} }
# Check available route helpers
AdminPortal::Engine.routes.url_helpers.methods.grep(/path|url/)Common Issues and Solutions
Missing Nested Routes:
# Ensure the association exists
User.reflect_on_association(:posts) # Should not be nil
# Check association route discovery
User.has_many_association_routes # Should include "posts"Incorrect Entity Scoping:
# Verify engine configuration
AdminPortal::Engine.scoped_to_entity? # => true
AdminPortal::Engine.scoped_entity_class # => Organization
AdminPortal::Engine.scoped_entity_strategy # => :pathInteractive Action Routes Missing:
# Ensure action is defined in resource definition
PostDefinition.new.defined_actions.keys # Should include your actionRoute Helper Not Found:
# Include the engine's route helpers
include AdminPortal::Engine.routes.url_helpers
# Test URL generation
posts_path # => "/posts" or "/organizations/:organization_id/posts"Best Practices
Route Organization
Register Resources Logically:
# ✅ Good - logical grouping
AdminPortal::Engine.routes.draw do
# Core entities first
register_resource Organization
register_resource User
# Business domain resources
register_resource Project
register_resource Task
# Supporting resources
register_resource Comment
register_resource Attachment
endLeverage Entity Scoping:
# ✅ Good - consistent scoping strategy
class AdminPortal::Engine < Rails::Engine
scope_to_entity Organization, strategy: :path
end
# All resources automatically scoped to organization
# Consistent URL structure: /:organization_id/resourcesSecurity Considerations
# ✅ Good - proper scoping for multi-tenancy
scope_to_entity Organization, strategy: :path
# ✅ Good - route-level authentication
constraints Rodauth::Rails.authenticate(:admin) do
mount AdminPortal::Engine, at: "/admin"
end
# ✅ Good - controller-level authorization
class PostsController < ApplicationController
include Plutonium::Resource::Controller
private
def current_authorized_scope
super.where(organization: current_scoped_entity)
end
endIntegration with Other Modules
With Resource Module
Routes automatically integrate with resource definitions:
class PostDefinition < Plutonium::Resource::Definition
# These create interactive action routes automatically
action :publish, interaction: PublishPostInteraction
action :archive, interaction: ArchivePostInteraction
endWith Portal Module
Portals provide routing contexts and scoping:
module AdminPortal
class Engine < Rails::Engine
include Plutonium::Portal::Engine
# This affects all routes in this portal
scope_to_entity Organization, strategy: :path
end
endWith Authentication Module
Routes can be protected by authentication constraints:
Rails.application.routes.draw do
# Only authenticated admins can access admin routes
constraints Rodauth::Rails.authenticate(:admin) do
mount AdminPortal::Engine, at: "/admin"
end
# Customer authentication for customer portal
constraints Rodauth::Rails.authenticate(:customer) do
mount CustomerPortal::Engine, at: "/app"
end
endRelated Modules
The Routing module works seamlessly with other Plutonium components:
- Controller: HTTP request handling and URL generation methods
- Resource Record: Resource definitions that drive route generation
- Portal: Multi-tenant portal functionality and route scoping
- Action: Interactive actions that create dynamic routes
- Authentication: Route protection and authentication constraints
