| name | routing |
| description | Expert guidance for defining routes in Rails applications. Use when adding routes, working with resources, nested routes, namespaces, path helpers, routes.rb, RESTful design, API routes, URL helpers, or any routing-related task. Covers resources, singular resources, nesting, namespace vs scope, constraints, concerns, member/collection routes, and route testing. |
| allowed-tools | Read, Grep, Glob, Write, Edit, Bash(bin/rails routes*), Bash(bin/rails test test/routing*) |
Rails Routing Expert
Define clean, RESTful, minimal routes. Extra routes create unnecessary attack surface and confuse other developers reading routes.rb.
Philosophy
- RESTful by default — Use
resources and resource. Custom routes are a smell.
- Minimal surface area — Always use
only: or except: to expose just what's needed.
- Path helpers everywhere — Hardcoded URL strings break when routes change. Use
_path and _url helpers.
- Shallow nesting only — Don't nest resources more than 1 level deep; deeper nesting creates painful URLs and helper names.
- New resource > custom action — If you're adding many custom actions, you need a new controller.
When To Use This Skill
- Adding or modifying routes in
config/routes.rb
- Deciding between namespace, scope, and module
- Nesting resources
- Adding member or collection routes
- Setting up API versioning
- Debugging route conflicts
- Testing routes
Instructions
Step 1: Check Existing Routes
Inspect current routes before adding new ones — duplicates and conflicts cause subtle bugs:
bin/rails routes
bin/rails routes -c users
bin/rails routes -g admin
bin/rails routes --unused
Step 2: Use Resources Correctly
resources (plural) — For collections
Generates 7 routes: index, show, new, create, edit, update, destroy.
resources :articles
resources :articles, only: [:index, :show, :create]
Ask: which actions does this controller actually implement? Only route those.
resource (singular) — For singletons
Use when there's only ONE of something per user/context. No index action. No :id in URLs.
resource :profile, only: [:show, :edit, :update]
resource :session, only: [:new, :create, :destroy]
resource :dashboard, only: :show
⚠️ CRITICAL: resource (singular) still routes to a PLURAL controller.
resource :profile → ProfilesController (not ProfileController)
Path helpers are singular: profile_path (not profiles_path), edit_profile_path.
When to use singular vs plural:
- Can the user have/see multiple? →
resources (plural)
- Is there exactly one in context? →
resource (singular)
- Does an
index action make sense? → resources (plural)
Step 3: Nest Resources (Max 1 Level Deep)
Nest when a child resource only makes sense within its parent.
resources :articles do
resources :comments, only: [:index, :create, :destroy]
end
resources :users do
resources :articles do
resources :comments
end
end
Use shallow nesting when child has its own identity:
resources :articles do
resources :comments, shallow: true
end
This gives you the parent context where it matters (creating/listing) and clean URLs for direct access.
Nested path helpers include the parent:
article_comments_path(@article)
article_comment_path(@article, @comment)
comment_path(@comment)
Step 4: Namespace vs Scope vs Module
These three look similar but behave differently — mixing them up causes confusing routing bugs.
namespace — Changes EVERYTHING (path + module + helpers)
namespace :admin do
resources :articles, only: [:index, :show]
end
- Path:
/admin/articles
- Controller:
Admin::ArticlesController (in app/controllers/admin/)
- Helper:
admin_articles_path
Use for: Admin panels, API versions, distinct sections with their own controllers.
scope module: — Changes MODULE only (not path)
scope module: :admin do
resources :articles, only: [:index, :show]
end
- Path:
/articles (no prefix!)
- Controller:
Admin::ArticlesController
- Helper:
articles_path
Use for: Organizing controllers into subdirectories without changing URLs.
scope path: — Changes PATH only (not module)
scope "/admin" do
resources :articles, only: [:index, :show]
end
- Path:
/admin/articles
- Controller:
ArticlesController (no module!)
- Helper:
articles_path
Use for: URL prefixes without separate controller namespaces.
Quick Reference
| Method | URL prefix | Controller module | Helper prefix |
|---|
namespace :admin | /admin | Admin:: | admin_ |
scope module: :admin | none | Admin:: | none |
scope "/admin" | /admin | none | none |
When in doubt, use namespace. It's the most explicit and least surprising.
Step 5: Member vs Collection Routes
When you need routes beyond the standard 7 RESTful actions.
Member routes — Act on ONE specific record (requires :id)
resources :articles, only: [:index, :show] do
member do
patch :publish
patch :archive
end
end
Generates: publish_article_path(@article), archive_article_path(@article)
Collection routes — Act on the collection (no :id)
resources :articles, only: [:index, :show] do
collection do
get :search
get :drafts
delete :clear_all
end
end
Generates: search_articles_path, drafts_articles_path
Shorthand (single route)
resources :photos do
get :preview, on: :member
get :search, on: :collection
end
⚠️ STOP — Do you actually need a custom action?
If you're adding more than 2 custom actions, you probably need a new resource:
resources :articles do
member do
patch :publish
patch :unpublish
patch :archive
patch :unarchive
patch :feature
end
end
resources :articles, only: [:index, :show]
resources :article_publications, only: [:create, :destroy]
resources :article_archives, only: [:create, :destroy]
Step 6: Route Concerns (DRY Shared Patterns)
When multiple resources share the same nested routes:
concern :commentable do
resources :comments, only: [:index, :create, :destroy]
end
concern :taggable do
resources :tags, only: [:index, :create, :destroy]
end
resources :articles, concerns: [:commentable, :taggable]
resources :photos, concerns: [:commentable]
resources :videos, concerns: [:commentable, :taggable]
Step 7: Constraints
Segment constraints (validate params)
resources :users, constraints: { id: /\d+/ }
Request constraints (subdomain, format, etc.)
constraints subdomain: "api" do
namespace :api do
resources :articles, only: [:index, :show]
end
end
Advanced constraints (custom logic)
get "*path", to: "errors#not_found",
constraints: lambda { |req| !req.path.start_with?("/admin") }
class ApiConstraint
def matches?(request)
request.headers["Accept"]&.include?("application/json")
end
end
constraints ApiConstraint.new do
resources :articles, only: [:index, :show]
end
Step 8: Root Route
Always define a root route:
root "pages#home"
Namespaced roots:
namespace :admin do
root "dashboard#show"
end
Step 9: Redirects
get "/old-path", to: redirect("/new-path")
get "/old-path", to: redirect("/new-path", status: 302)
get "/articles/:id", to: redirect("/posts/%{id}")
Step 10: Mount Engines
mount Sidekiq::Web => "/sidekiq"
mount ActionCable.server => "/cable"
Step 11: API Routes
namespace :api do
namespace :v1 do
resources :articles, only: [:index, :show, :create, :update, :destroy]
resources :users, only: [:show]
end
end
For API-only controllers, use defaults: { format: :json }:
namespace :api, defaults: { format: :json } do
namespace :v1 do
resources :articles, only: [:index, :show]
end
end
Step 12: Direct and Resolve (Custom URL Helpers)
direct — Create custom URL helpers
direct :homepage do
"https://example.com"
end
direct :cdn_image do |model|
"https://cdn.example.com/#{model.image_path}"
end
resolve — Customize polymorphic routing for models
Required when using resource (singular) with form_with:
resource :basket, only: [:show, :update]
resolve("Basket") { [:basket] }
Route File Organization
Order your config/routes.rb like this:
Rails.application.routes.draw do
root "pages#home"
resource :session, only: [:new, :create, :destroy]
resources :registrations, only: [:new, :create]
resources :articles do
resources :comments, only: [:index, :create], shallow: true
end
namespace :admin do
root "dashboard#show"
resources :users
end
namespace :api, defaults: { format: :json } do
namespace :v1 do
resources :articles, only: [:index, :show]
end
end
get "up", to: "rails/health#show", as: :rails_health_check
get "*path", to: "errors#not_found"
end
Testing Routes
class ArticlesRoutingTest < ActionDispatch::IntegrationTest
test "routes to articles#index" do
assert_routing "/articles", controller: "articles", action: "index"
end
test "routes to articles#show" do
assert_routing "/articles/1", controller: "articles", action: "show", id: "1"
end
test "generates correct path" do
assert_generates "/articles/1", controller: "articles", action: "show", id: "1"
end
test "recognizes route" do
assert_recognizes(
{ controller: "articles", action: "create" },
{ path: "/articles", method: :post }
)
end
end
Anti-Patterns
- Generating all 7 routes when you need 2 — Always use
only: or except:
- Nesting deeper than 1 level — Flatten with shallow or separate resources
- Hardcoding paths — Hardcoded strings break when routes change; use
article_path(@article) instead of "/articles/#{@article.id}"
- Using
match ... via: :all — Exposing all HTTP verbs creates security surface; be explicit
- Too many custom member/collection actions — Extract a new resource instead
- Confusing namespace/scope/module — Check the table in Step 4
resource when you mean resources — Singular = one thing, no index, no :id
- Missing
only:/except: on nested resources — Nested routes bloat fast
- Forgetting
resolve with singular resource — form_with will break without it
- Route order bugs — More specific routes go above general ones (Rails matches top-down, first match wins)
Debugging
bin/rails routes -g "GET /articles"
bin/rails routes | grep articles
Rails.application.routes.url_helpers.articles_path
app.edit_article_path(Article.first)
For detailed patterns, examples, and edge cases, see reference.md in this skill directory.