skip to content

In a Rails app where photos are published and unpublished, why route to a separate resourceful controller instead of adding publish and unpublish actions to PhotosController?

level: seniorimportance: should knowfreq 35%

answer

  1. turn the verb into a noun
  2. create publishes, destroy unpublishes
  3. PhotosController keeps seven actions
  4. resource :publication with only:
  5. smaller controllers, focused filters

basics

~20 s

Modelling publishing as a resource, resource :publication under each photo, maps publish to create and unpublish to destroy on a small controller, so every controller keeps to the seven actions and the routes stay predictable.

solid answer

~30 s

The resourceful habit is to turn a verb into a noun. Publishing a photo creates a publication; unpublishing destroys it. So instead of `publish` and `unpublish` actions on `PhotosController`, draw `resource :publication, only: [:create, :destroy], module: :photos` inside the photos block: `POST /photos/:photo_id/publication` reaches `Photos::PublicationsController#create` and `DELETE` reaches `destroy`. The gains are structural: `PhotosController` keeps only the seven standard actions, the new controller has its own filters and authorization, verbs carry meaning (POST creates, DELETE removes), and the routes follow the same table everyone already knows. A custom member route stays fine for a one-off read, such as a download.

code

ruby · 24 lines
ruby
# config/routes.rb
resources :photos do
  resource :publication, only: [:create, :destroy], module: :photos
end

# app/controllers/photos/publications_controller.rb
class Photos::PublicationsController < ApplicationController
  before_action :set_photo

  def create
    @photo.update!(published_at: Time.current)
    redirect_to @photo
  end

  def destroy
    @photo.update!(published_at: nil)
    redirect_to @photo
  end

  private
    def set_photo
      @photo = Photo.find(params[:photo_id])
    end
end

go deeper

for a junior

Know that a state change can be written as creating or destroying a small resource, and that this keeps controllers to the seven standard actions.

for a middle

Draw the routes for resource :publication under photos and name the controller, verbs and helper it produces.

for a senior

Argue the design: focused filters and authorization, honest HTTP verbs, and when a custom member route is still the simpler answer.

for a principal

Set the convention for the codebase, weighing a consistent seven-action vocabulary against the number of small controllers it produces.

## The problem with a growing verb list A photography portfolio starts with `resources :photos` in `config/routes.rb`. Then the product asks for publishing, unpublishing, featuring on the home page, archiving and restoring. The quick answer adds an action per verb to `PhotosController`, each with its own custom route. After a year the controller holds a dozen actions with different filters, different permission rules and different templates, and the route table no longer follows the seven-action pattern anyone can predict. The resourceful alternative, often summarised as **"a controller over a verb"**, asks a different question: **which thing is created or destroyed when this verb happens?** ## Verbs become nouns | Verb on a photo | Noun | Routes | Action | |---|---|---|---| | publish | publication | `POST /photos/:photo_id/publication` | `create` | | unpublish | publication | `DELETE /photos/:photo_id/publication` | `destroy` | | feature | feature | `POST /photos/:photo_id/feature` | `create` | | archive | archival | `POST /photos/:photo_id/archival` | `create` | Each noun gets a small resource, usually singular because a photo has at most one publication, trimmed to the actions it needs. ```ruby resources :photos do resource :publication, only: [:create, :destroy], module: :photos end ``` That line draws two routes, both on `/photos/:photo_id/publication`, handled by `Photos::PublicationsController`, with the helper `photo_publication_path(photo)`. ## What the separate controller buys - **Standard actions everywhere.** `PhotosController` keeps `index` through `destroy`; the publication controller has `create` and `destroy`. Nobody has to learn a custom action name. - **Focused filters and authorization.** Only photographers may publish; anyone may view. That rule sits on one small controller instead of an `only:` list on a `before_action` in a large one. - **Meaningful HTTP verbs.** `POST` creates the state and `DELETE` removes it, so a repeated `DELETE` reads as "make sure it is unpublished", and caches and logs see honest methods. - **Room to grow.** If publishing later takes a scheduled date, the `create` action and its form already have a home. - **Predictable tests.** Each controller test covers a short, standard action list. The model need not change: a `published_at` column on photos can back the "publication" resource perfectly well. The noun lives in the routing and controller layer, not necessarily as a table. ## When a custom route is still fine The pattern is a default, not a law. A custom member or collection route remains reasonable when: 1. the action is a **read** that is a different representation of the same record, such as a full-resolution download; 2. it is genuinely **one-off** and unlikely to grow its own rules; 3. the noun would be contrived enough to confuse readers more than an action name would. Interviewers listen for the reasoning: does the candidate notice that a state change is really a create or destroy of something, and can they say why that keeps controllers small? ## How the views talk to it The view side stays as plain as the routes. A publish button submits to the new route with the verb that matches the change: `POST` to `photo_publication_path(@photo)` to publish, `DELETE` to the same path to unpublish. Nothing in the template needs a custom action name, and the controller's `create` and `destroy` answer with a redirect back to the photo. Because both buttons share one path and differ only in verb, the route table reads exactly like the product rule: a photo either has a publication or it does not. ## Refactoring an existing verb action Moving an app that already has `post :publish, on: :member` to the resourceful form takes a few steps: 1. Draw `resource :publication, only: [:create, :destroy], module: :photos` inside the photos block, next to the old route. 2. Move the body of `publish` into `Photos::PublicationsController#create` and `unpublish` into `destroy`, with their filters. 3. Point the buttons at `photo_publication_path(@photo)` with the matching verb, then delete the old routes and actions once nothing links to them. ## Mistakes to avoid - Using `GET` for the state change, which lets prefetchers and crawlers trigger it. - Putting the new resource outside the photo's path, which loses `params[:photo_id]` and forces the client to send the id in the body. - Building a `resources :publications` collection with ids when the photo can only have one; the singular form fits the data.

  • Why resource :publication rather than resources :publications here?
    A photo has at most one publication, so there is no collection to list and no publication id worth putting in the URL. The singular form draws `/photos/:photo_id/publication` with no id, matching the data, and `only: [:create, :destroy]` keeps it to the two state changes the product needs.
  • When would you still add a custom member route to PhotosController?
    For a read that is another view of the same photo, such as a full-resolution download, or a genuine one-off with no rules of its own. Those do not create or destroy anything, so inventing a noun for them adds a controller without making the design clearer.

A library does not give each book a 'borrow' button and a 'return' button; it opens a loan and later ends it. The book stays a book, and loans get their own desk with their own rules.

saying these in an interview costs you the question

  • Every new verb on a photo needs a new action on PhotosController
  • A separate resource needs its own database table
  • GET /photos/7/publish is fine because it is only a link
  • resources :publications with ids is needed even when a photo has one publication
  • only: [:create, :destroy] can also accept :publish and :unpublish