Skip
Arish's avatar

36. RESTful Controllers


RESTful Controllers

REST (Representational State Transfer) is an architectural style that Rails embraces fully. RESTful controllers follow conventions that make your code predictable and maintainable.

The Seven RESTful Actions

ruby
1class ArticlesController < ApplicationController
2  # GET /articles
3  def index
4    @articles = Article.all
5  end
6  
7  # GET /articles/:id
8  def show
9    @article = Article.find(params[:id])
10  end
11  
12  # GET /articles/new
13  def new
14    @article = Article.new
15  end
16  
17  # POST /articles
18  def create
19    @article = Article.new(article_params)
20    
21    if @article.save
22      redirect_to @article, notice: "Article created!"
23    else
24      render :new, status: :unprocessable_entity
25    end
26  end
27  
28  # GET /articles/:id/edit
29  def edit
30    @article = Article.find(params[:id])
31  end
32  
33  # PATCH/PUT /articles/:id
34  def update
35    @article = Article.find(params[:id])
36    
37    if @article.update(article_params)
38      redirect_to @article, notice: "Article updated!"
39    else
40      render :edit, status: :unprocessable_entity
41    end
42  end
43  
44  # DELETE /articles/:id
45  def destroy
46    @article = Article.find(params[:id])
47    @article.destroy
48    redirect_to articles_url, notice: "Article deleted!"
49  end
50  
51  private
52  
53  def article_params
54    params.require(:article).permit(:title, :body, :published)
55  end
56end

RESTful Routes

ruby
1# config/routes.rb
2Rails.application.routes.draw do
3  resources :articles
4end

This generates:

HTTP VerbPathActionPurpose
GET/articlesindexList all
GET/articles/newnewForm for new
POST/articlescreateCreate new
GET/articles/:idshowShow one
GET/articles/:id/editeditForm to edit
PATCH/PUT/articles/:idupdateUpdate one
DELETE/articles/:iddestroyDelete one

Nested Resources

ruby
1# Routes
2resources :articles do
3  resources :comments
4end
5
6# Controller
7class CommentsController < ApplicationController
8  before_action :set_article
9  
10  # GET /articles/:article_id/comments
11  def index
12    @comments = @article.comments
13  end
14  
15  # POST /articles/:article_id/comments
16  def create
17    @comment = @article.comments.build(comment_params)
18    
19    if @comment.save
20      redirect_to @article, notice: "Comment added!"
21    else
22      render "articles/show"
23    end
24  end
25  
26  # DELETE /articles/:article_id/comments/:id
27  def destroy
28    @comment = @article.comments.find(params[:id])
29    @comment.destroy
30    redirect_to @article, notice: "Comment deleted!"
31  end
32  
33  private
34  
35  def set_article
36    @article = Article.find(params[:article_id])
37  end
38  
39  def comment_params
40    params.require(:comment).permit(:body)
41  end
42end

Shallow Nesting

Avoid deeply nested routes:

ruby
1# Routes
2resources :articles do
3  resources :comments, shallow: true
4end
5
6# Generates:
7# /articles/:article_id/comments     (index, new, create)
8# /comments/:id                      (show, edit, update, destroy)

Custom Actions

Member Actions (on a specific resource)

ruby
1# Routes
2resources :articles do
3  member do
4    post :publish
5    post :unpublish
6    get :preview
7  end
8end
9
10# Or single action
11resources :articles do
12  post :publish, on: :member
13end
14
15# Controller
16class ArticlesController < ApplicationController
17  # POST /articles/:id/publish
18  def publish
19    @article = Article.find(params[:id])
20    @article.update(published: true, published_at: Time.current)
21    redirect_to @article, notice: "Article published!"
22  end
23  
24  # GET /articles/:id/preview
25  def preview
26    @article = Article.find(params[:id])
27    render layout: "preview"
28  end
29end

Collection Actions (on the collection)

ruby
1# Routes
2resources :articles do
3  collection do
4    get :search
5    get :published
6    get :drafts
7    delete :clear_old
8  end
9end
10
11# Controller
12class ArticlesController < ApplicationController
13  # GET /articles/search
14  def search
15    @articles = Article.where("title LIKE ?", "%#{params[:q]}%")
16    render :index
17  end
18  
19  # GET /articles/published
20  def published
21    @articles = Article.where(published: true)
22    render :index
23  end
24  
25  # DELETE /articles/clear_old
26  def clear_old
27    Article.where("created_at < ?", 1.year.ago).destroy_all
28    redirect_to articles_path, notice: "Old articles cleared!"
29  end
30end

Respond To Different Formats

ruby
1class ArticlesController < ApplicationController
2  def index
3    @articles = Article.all
4    
5    respond_to do |format|
6      format.html # renders index.html.erb
7      format.json { render json: @articles }
8      format.xml { render xml: @articles }
9      format.csv { send_data @articles.to_csv }
10    end
11  end
12  
13  def show
14    @article = Article.find(params[:id])
15    
16    respond_to do |format|
17      format.html
18      format.json { render json: @article }
19      format.pdf do
20        pdf = ArticlePdf.new(@article)
21        send_data pdf.render, filename: "#{@article.slug}.pdf"
22      end
23    end
24  end
25end

Turbo Stream Responses (Rails 7+)

ruby
1class CommentsController < ApplicationController
2  def create
3    @article = Article.find(params[:article_id])
4    @comment = @article.comments.build(comment_params)
5    
6    if @comment.save
7      respond_to do |format|
8        format.turbo_stream
9        format.html { redirect_to @article }
10      end
11    else
12      render :new
13    end
14  end
15end
erb
1<!-- app/views/comments/create.turbo_stream.erb -->
2<%= turbo_stream.prepend "comments", @comment %>
3<%= turbo_stream.update "comment_form", partial: "comments/form", locals: { comment: Comment.new } %>

Error Handling

ruby
1class ArticlesController < ApplicationController
2  rescue_from ActiveRecord::RecordNotFound, with: :not_found
3  rescue_from ActionController::ParameterMissing, with: :bad_request
4  
5  def show
6    @article = Article.find(params[:id])
7  end
8  
9  private
10  
11  def not_found
12    respond_to do |format|
13      format.html { render "errors/not_found", status: :not_found }
14      format.json { render json: { error: "Not found" }, status: :not_found }
15    end
16  end
17  
18  def bad_request(exception)
19    render json: { error: exception.message }, status: :bad_request
20  end
21end

Skinny Controllers

Keep controllers focused. Move logic to models or services:

ruby
1# Fat controller (bad)
2class ArticlesController < ApplicationController
3  def create
4    @article = Article.new(article_params)
5    @article.user = current_user
6    @article.published_at = Time.current if params[:publish]
7    @article.slug = @article.title.parameterize
8    
9    if @article.save
10      ArticleMailer.new_article(@article).deliver_later
11      AdminNotifier.notify(@article)
12      redirect_to @article
13    else
14      render :new
15    end
16  end
17end
18
19# Skinny controller (good)
20class ArticlesController < ApplicationController
21  def create
22    @article = current_user.articles.build(article_params)
23    
24    if @article.save
25      @article.notify_subscribers
26      redirect_to @article
27    else
28      render :new
29    end
30  end
31end

RESTful controllers make your Rails applications predictable and easy to maintain!