Working with Nested Resources in Ruby on Rails

2 min read

Imagine you're building a course platform, where each course has multiple lessons.

  • Each lesson is associated with a particular course.
  • Whenever you perform an operation on a lesson (create, read, update, delete), you're working in the context of an existing course.
  • When working with a lesson, you're really working with a course-lesson pair.

Basically, the lesson resource is nested under the course resource. In a Rails routing file, you can represent the relationship between a course and its lessons as follows:

# config/routes.rb
resources :courses do
  resources :lessons
end

This instructs the Rails router to generate regular resourceful routes for the course resource, and generate nested, resourceful routes for the lesson resource. Let's check the routes generated for the lesson resource.

$ bin/rails routes -g lesson
                Prefix Verb   URI Pattern                                    Controller#Action
        course_lessons GET    /courses/:course_id/lessons(.:format)          lessons#index
                       POST   /courses/:course_id/lessons(.:format)          lessons#create
     new_course_lesson GET    /courses/:course_id/lessons/new(.:format)      lessons#new
    edit_course_lesson GET    /courses/:course_id/lessons/:id/edit(.:format) lessons#edit
         course_lesson GET    /courses/:course_id/lessons/:id(.:format)      lessons#show
                       PATCH  /courses/:course_id/lessons/:id(.:format)      lessons#update
                       PUT    /courses/:course_id/lessons/:id(.:format)      lessons#update
                       DELETE /courses/:course_id/lessons/:id(.:format)      lessons#destroy

As you can see, all lesson route URLs are prefixed with /courses/:course_id, meaning each lesson is nested under a particular course whose ID can be accessed by params[:course_id].

In addition, all lesson route names are prefixed with "course", providing clear and obvious named route helpers, such as: course_lessons_path and new_course_lesson_path. When using a named route helper, you must provide a valid instance of the Course resource class under which this lesson is nested. For example,

link_to "Add New Lesson", new_course_lesson_path(course)
link_to "Edit Lesson", edit_course_lesson_path(course, lesson)

Given a course with ID 5 and a lesson with ID 10, the URL for that particular lesson will look like: /courses/5/lessons/10, following the structure: /courses/{course_id}/lessons/{id}. The controller can access the course id and lesson id using params[:course_id] and params[:id].

Namespacing a Nested Route

In the above example, the lessons resource will use the LessonsController class (located under the app/controllers directory). If you want to have the LessonsController class nested under the app/controllers/courses directory instead, pass the :module option as follows:

# config/routes.rb
resources :courses do
  resources :lessons, module: "courses"
end

Let's inspect the routes generated by the above configuration.

bin/rails routes -g lesson
                Prefix Verb   URI Pattern                                    Controller#Action
        course_lessons GET    /courses/:course_id/lessons(.:format)          courses/lessons#index
                       POST   /courses/:course_id/lessons(.:format)          courses/lessons#create
     new_course_lesson GET    /courses/:course_id/lessons/new(.:format)      courses/lessons#new
    edit_course_lesson GET    /courses/:course_id/lessons/:id/edit(.:format) courses/lessons#edit
         course_lesson GET    /courses/:course_id/lessons/:id(.:format)      courses/lessons#show
                       PATCH  /courses/:course_id/lessons/:id(.:format)      courses/lessons#update
                       PUT    /courses/:course_id/lessons/:id(.:format)      courses/lessons#update
                       DELETE /courses/:course_id/lessons/:id(.:format)      courses/lessons#destroy

Notice the only column that changed was the Controller#Action column. It's now using the Courses::LessonsController class nested under the app/controllers/courses/lessons_controller.rb file.

A word of caution: resources should never be nested more than 1 level deep. Why? Check out this excellent post from Jamis Buck.