Archive:

Rails Routing: RESTful Routes and Beyond


Rails Routing: RESTful Routes and Beyond

Rails routing maps URLs to controller actions. Understanding routing is critical for building clean, maintainable APIs.

Basic RESTful Routing

Resource Declaration

# config/routes.rb
Rails.application.routes.draw do
  resources :posts              # Generates 7 RESTful routes
  resources :comments           # Nested routes possible
  resources :users              # Standard REST endpoints
end

This creates 7 routes automatically:

HTTP Method URL Controller#Action Purpose
GET /posts posts#index List all posts
GET /posts/new posts#new Form for new post
POST /posts posts#create Create post
GET /posts/:id posts#show View single post
GET /posts/:id/edit posts#edit Edit form
PATCH/PUT /posts/:id posts#update Update post
DELETE /posts/:id posts#destroy Delete post

Customizing Resources

# Limit actions
resources :posts, only: [:index, :show]
resources :comments, except: [:destroy]

# Rename path but keep model name
resources :articles, path: 'blog_posts'    # /blog_posts instead of /articles

# Change controller
resources :posts, controller: 'articles'   # Maps to ArticlesController

# Add custom action
resources :posts do
  member do
    patch :publish              # PATCH /posts/:id/publish
    post :archive               # POST /posts/:id/archive
  end

  collection do
    get :trending               # GET /posts/trending
    get :search                 # GET /posts/search
  end
end

Nested Routes

Use nested routes for hierarchical resources:

# config/routes.rb
resources :users do
  resources :posts              # GET /users/:user_id/posts
end

resources :posts do
  resources :comments           # GET /posts/:post_id/comments
  resources :likes              # GET /posts/:post_id/likes
end

Shallow Nesting (Best Practice)

Deep nesting becomes unwieldy. Use shallow nesting:

# BAD - Too deep
resources :users do
  resources :posts do
    resources :comments         # /users/:user_id/posts/:post_id/comments
  end
end

# GOOD - Shallow nesting
resources :users do
  resources :posts, shallow: true do
    resources :comments
  end
end

# Results in:
# /users/:user_id/posts           (index, new, create)
# /posts/:id                       (show, edit, update, delete)
# /posts/:post_id/comments         (index, new, create)
# /comments/:id                    (show, edit, update, delete)

Route Constraints

Limit routes based on parameters:

# Constraint on parameter format
resources :posts, constraints: { id: /\d+/ }   # Only numeric IDs

# Custom constraint
class AdminConstraint
  def matches?(request)
    request.session[:user_id] && User.find(request.session[:user_id]).admin?
  end
end

constraints AdminConstraint.new do
  resources :admin_panel
end

# Domain-based routing
namespace :admin do
  root 'dashboard#index'
  resources :users
end

# Subdomain routing
constraints(subdomain: 'api') do
  namespace :api do
    resources :posts
  end
end

Scoped Routes

Group related routes without creating a new resource:

# Namespace - creates module and path prefix
namespace :admin do
  resources :users              # Admin::UsersController at /admin/users
  resources :posts              # Admin::PostsController at /admin/posts
end

# Scope - only path prefix, no module
scope :api do
  resources :posts              # PostsController at /api/posts
end

# Scope with module
scope :api, module: 'api' do
  resources :posts              # Api::PostsController at /api/posts
end

Advanced Routing Patterns

Singleton Routes

When only one instance exists:

resource :profile                # No index, no ID in path
resource :settings               # GET /profile, PATCH /profile
resource :dashboard              # No collection

# Routes generated:
# GET /profile         → profiles#show
# GET /profile/edit    → profiles#edit
# PATCH /profile       → profiles#update
# DELETE /profile      → profiles#destroy

Glob Routes (Catch-All)

# Match any remaining segments
get '/docs/*page', to: 'docs#show'

# Matches:
# /docs/getting-started → params[:page] = "getting-started"
# /docs/api/v1          → params[:page] = "api/v1"

Redirect Routes

# Permanent redirect (301)
get '/old-path', to: redirect('/new-path')

# With parameters
get '/users/:id', to: redirect('/profiles/%{id}')

# Conditional redirect with block
get '/legacy/*path', to: redirect { |params, request|
  "/api/v2/#{params[:path]}"
}

Root Routes & Named Routes

# Root route
root 'pages#home'               # GET / → pages#home

# Named routes
get 'about', to: 'pages#about', as: 'about'
post 'contact', to: 'pages#contact', as: 'create_contact'

# In views:
<%= link_to 'About', about_path %>
<%= link_to 'Home', root_path %>

HTTP Verb Routing

# Explicit verb matching
get 'search', to: 'search#new'
post 'search', to: 'search#create'

# Multiple verbs
match 'search', to: 'search#index', via: [:get, :post]

# Match all verbs
match 'catch-all', to: 'pages#error', via: :all

Generating URLs

# In controllers/views
link_to 'Show', post_path(@post)
link_to 'Edit', edit_post_path(@post)
link_to 'Delete', post_path(@post), method: :delete

# With nested routes
user_posts_path(@user)                    # /users/:user_id/posts
user_post_path(@user, @post)              # /users/:user_id/posts/:id
edit_user_post_path(@user, @post)         # /users/:user_id/posts/:id/edit

# In redirects
redirect_to post_path(@post)
redirect_to user_posts_path(@user)

Common Routing Mistakes

# BAD - Wrong order (more specific should come first)
resources :posts
get '/posts/trending'           # Never matched! Resources catch it first

# GOOD - Specific routes first
get '/posts/trending', to: 'posts#trending'
resources :posts

# BAD - Overcomplicated nesting
resources :companies do
  resources :departments do
    resources :teams do
      resources :members
    end
  end
end

# GOOD - Shallow after 2 levels
resources :companies do
  resources :departments, shallow: true do
    resources :teams, shallow: true do
      resources :members
    end
  end
end

# BAD - Forgot to generate routes after changes
# (common mistake: routes cached in development)

# GOOD - Clear route structure
namespace :api do
  namespace :v1 do
    resources :posts
    resources :comments
  end
end

Testing Routes

# config/routes.rb is a file, test it directly

# In spec/routing/posts_routing_spec.rb
describe 'posts routing' do
  it 'routes to posts#index' do
    expect(get: '/posts').to route_to('posts#index')
  end

  it 'routes to posts#show' do
    expect(get: '/posts/1').to route_to('posts#show', id: '1')
  end

  it 'routes to posts#create' do
    expect(post: '/posts').to route_to('posts#create')
  end
end

Route Debugging

# View all routes
rails routes

# Filter routes
rails routes -c posts          # Only posts controller
rails routes -g post           # Matching pattern

# In Rails console
app.routes.url_helpers.posts_path
app.routes.url_helpers.post_path(1)

Conclusion

Rails routing principles:

  • Use RESTful conventions by default
  • Keep nesting shallow (max 2 levels deep)
  • Use namespaces for logical grouping
  • Prefer named routes over hardcoded paths
  • Test critical routing logic
  • Use rails routes to audit your routes