Agent Skills: Rails Routing Expert

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.

UncategorizedID: thinkoodle/rails-skills/routing

Install this agent skill to your local

pnpm dlx add-skill https://github.com/ThinkOodle/rails-skills/tree/HEAD/skills/routing

Skill Files

Browse the full folder contents for routing.

Download Skill

Loading file tree…

skills/routing/SKILL.md

Skill Metadata

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.

Rails Routing Expert

Define clean, RESTful, minimal routes. Extra routes create unnecessary attack surface and confuse other developers reading routes.rb.

Philosophy

  1. RESTful by default — Use resources and resource. Custom routes are a smell.
  2. Minimal surface area — Always use only: or except: to expose just what's needed.
  3. Path helpers everywhere — Hardcoded URL strings break when routes change. Use _path and _url helpers.
  4. Shallow nesting only — Don't nest resources more than 1 level deep; deeper nesting creates painful URLs and helper names.
  5. 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:

# Show all routes
bin/rails routes

# Filter by controller
bin/rails routes -c users

# Search by path or helper name
bin/rails routes -g admin

# Find unused routes (clean these up!)
bin/rails routes --unused

Step 2: Use Resources Correctly

resources (plural) — For collections

Generates 7 routes: index, show, new, create, edit, update, destroy.

# Generates all 7 routes — unused routes create dead endpoints and confuse devs
resources :articles

# Better — only generate what the controller actually implements
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.

# Current user's profile — there's only one
resource :profile, only: [:show, :edit, :update]

# Current user's session
resource :session, only: [:new, :create, :destroy]

# App-wide dashboard
resource :dashboard, only: :show

⚠️ CRITICAL: resource (singular) still routes to a PLURAL controller. resource :profileProfilesController (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.

# GOOD — 1 level deep
resources :articles do
  resources :comments, only: [:index, :create, :destroy]
end
# Avoid — 2+ levels creates unwieldy URLs and painful helper names
resources :users do
  resources :articles do
    resources :comments  # ← /users/1/articles/2/comments/3
  end
end

Use shallow nesting when child has its own identity:

# GOOD — collection routes nested, member routes flat
resources :articles do
  resources :comments, shallow: true
end
# Creates:
#   /articles/:article_id/comments     (index, create, new)
#   /comments/:id                      (show, edit, update, destroy)

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)           # GET /articles/1/comments
article_comment_path(@article, @comment)  # GET /articles/1/comments/5
# With shallow:
comment_path(@comment)                    # GET /comments/5

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 /articles/:id/publish
    patch :archive      # PATCH /articles/:id/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 /articles/search
    get :drafts         # GET /articles/drafts
    delete :clear_all   # DELETE /articles/clear_all
  end
end

Generates: search_articles_path, drafts_articles_path

Shorthand (single route)

resources :photos do
  get :preview, on: :member       # GET /photos/:id/preview
  get :search, on: :collection    # GET /photos/search
end

⚠️ STOP — Do you actually need a custom action?

If you're adding more than 2 custom actions, you probably need a new resource:

# BAD — too many custom actions on one resource
resources :articles do
  member do
    patch :publish
    patch :unpublish
    patch :archive
    patch :unarchive
    patch :feature
  end
end

# GOOD — extract a new resource
resources :articles, only: [:index, :show]
resources :article_publications, only: [:create, :destroy]  # publish/unpublish
resources :article_archives, only: [:create, :destroy]      # archive/unarchive

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)

# Lambda
get "*path", to: "errors#not_found",
  constraints: lambda { |req| !req.path.start_with?("/admin") }

# Object (responds to matches?)
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"  # /admin → Admin::DashboardController#show
end

Step 9: Redirects

# Simple redirect (301 by default)
get "/old-path", to: redirect("/new-path")

# With status code
get "/old-path", to: redirect("/new-path", status: 302)

# Dynamic redirect
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
# homepage_url => "https://example.com"

direct :cdn_image do |model|
  "https://cdn.example.com/#{model.image_path}"
end
# cdn_image_url(@photo) => "https://cdn.example.com/photos/1.jpg"

resolve — Customize polymorphic routing for models

Required when using resource (singular) with form_with:

resource :basket, only: [:show, :update]
resolve("Basket") { [:basket] }

# Now form_with(model: @basket) generates /basket (not /baskets/:id)

Route File Organization

Order your config/routes.rb like this:

Rails.application.routes.draw do
  # 1. Root
  root "pages#home"

  # 2. Authentication / sessions
  resource :session, only: [:new, :create, :destroy]
  resources :registrations, only: [:new, :create]

  # 3. Core resources (most used first)
  resources :articles do
    resources :comments, only: [:index, :create], shallow: true
  end

  # 4. Namespaced sections
  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

  # 5. Utility routes
  get "up", to: "rails/health#show", as: :rails_health_check

  # 6. Catch-all (if needed — put LAST)
  get "*path", to: "errors#not_found"
end

Testing Routes

# test/routing/articles_routing_test.rb
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

  1. Generating all 7 routes when you need 2 — Always use only: or except:
  2. Nesting deeper than 1 level — Flatten with shallow or separate resources
  3. Hardcoding paths — Hardcoded strings break when routes change; use article_path(@article) instead of "/articles/#{@article.id}"
  4. Using match ... via: :all — Exposing all HTTP verbs creates security surface; be explicit
  5. Too many custom member/collection actions — Extract a new resource instead
  6. Confusing namespace/scope/module — Check the table in Step 4
  7. resource when you mean resources — Singular = one thing, no index, no :id
  8. Missing only:/except: on nested resources — Nested routes bloat fast
  9. Forgetting resolve with singular resourceform_with will break without it
  10. Route order bugs — More specific routes go above general ones (Rails matches top-down, first match wins)

Debugging

# See what a specific URL maps to
bin/rails routes -g "GET /articles"

# Check for conflicts
bin/rails routes | grep articles

# In rails console
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.