Implementation of Nested and Un-nested Routes With RESTful Conventions

Samuel Guo
Jan 6 · 7 min read

Background

I am currently working on a group project, which is a web application where landlords can leave reviews of tenants. The schema is a many-to-many relationship between landlords and tenants where the join table is reviews. The group decided on using a MVC (model-view-controller) design pattern and Ruby on Rails for the backend. For my portion, I was responsible for writing the code of the Tenants Controller using RESTful conventions. The difficult part of this was figuring it out how to do it for a nested route (e.g., localhost:3000/landlords/2/tenants/4).

Although I know how to do it for an un-nested route (localhost:3000/tenants/4), this is my first attempt at making RESTful methods for a nested route. The solution was much simpler than I thought, but knowing my overthinking brain, I obviously had to overcomplicate it during my first attempt at it based on my understanding of routes.

First Attempt at the Tenants Controller with Nested Routes

At this point of my code, I had already completed the RESTful methods of the un-nested routes which looks like the following below:

class TenantsController < ApplicationController
def index
tenants = Tenant.all
render json: tenants
end
def show
id = params[:id]
tenant = Tenant.find(id)
render json: tenant
end
def create
# param keys may subject to change depending on the body of the post request
new_tenant = Tenant.new(first_name: params[:first_name], last_name: params[:last_name])
if new_tenant.valid?
Tenant.create(first_name: params[:first_name], last_name: params[:last_name])
# message can be altered for better UI experience in the future
render json: {message: "Tenant successfully created"}
else
# message can be altered for better UI experience in the future
render json: {message: "There was an error."}
end
end
def update
id = params[:id]
if Tenant.update(id, first_name: params[:first_name], last_name: params[:last_name]).valid?
Tenant.update(id, first_name: params[:first_name], last_name: params[:last_name])
updated_tenant = Tenant.find(id)
# message can be altered for better UI experience in the future
render json: {tenant: updated_tenant, message: "Tenant successfully updated"}
else
# message can be altered for better UI experience in the future
render json: {message: "Update failed"}
end
end
def destroy
id = params[:id]
Tenant.destroy(id)
render json: {message: "Successfully deleted"}
end
end

From here is where I attempted to do the Tenant Controller methods for the nested routes.

Also as a quick reference, the code for the nested routes looks like the following below:

resources :landlords do
resources :tenants
end

I used an example of a nested route, like “localhost:3000/landlords/2/tenants/4”, as a baseline for my thought process. The route, “localhost:3000/landlords/2/tenants/4”, means that the landlord whose id is 2 is logged in and is showing the tenant whose id is 4. This also implies that the landlord and tenants have a relationship with each other in the database. The 2 and 4 are also numbers that I can grab within the params hash when the route hits the Tenants Controller.

Typically, for an un-nested route, the numbers are represented by :id in the URL (i.e., tenants/:id). :id is the default variable provided by Rails when making routes. However, I was unsure how Rails did it for a nested route. Since the default variable is :id , I assumed that the template for a nested route would be like “localhost:3000/landlords/:id/tenants/:id” and Rails somehow knew how to differentiate the :id s between landlords and tenants.

Following this train of thought, I thought I would need to set my own custom variables (i.e., :landlord_id in lieu of :id ) for the URL routes to differentiate between the landlord and tenants ids. This meant that I would need to change the implicit routes from resources :tenants into its own customized HTTP verb, path, and instance methods. In other words, I would make a “copy” of the RESTful conventions and customize it for the nested routes.

The updated route would look like the following:

resources :landlords do
get "/tenants", to: "tenants#nested_index
get "/tenants/:tenant_id", to: "tenants#nested_show"
post "/tenants", to: "tenants#nested_create"
patch "/tenants/:tenant_id", to: "tenants#nested_update"
delete "/tenants/:tenant_id", to: "tenants#nested_destroy"
end

This would mean I would need to create five new additional methods, which essentially does the same as the five methods I already have.

I started to get the feeling that this was starting to get overcomplicated, so I decided to take a step back and re-evaluate my thoughts where I reached to two conclusions:

  1. Creating five new methods isn’t keeping the code DRY (don’t repeat yourself) where these methods achieve the same RESTful functionality but with only a slight difference to them. Knowing how Rails work, it was created to run a lot of code under the hood with as little written code as possible.
  2. The original routes code was already approved by a separate group member. Changing the approved code in order to achieve my tasks should be a last resort as that will cause backtracking with the code base and causing another code review.

Second Attempt at the Tenants Controller with Nested Routes

After coming to those two conclusions, I decided to go to the Rails documentation instead of attempting to re-invent the wheel (don’t want another experience similar to when I re-derived Rails validation methods). My research led to this documentation.

The following information from the documentation below was what I needed:

In addition to the routes for magazines, this declaration will also route ads to an AdsController. The ad URLs require a magazine:

Nested Routes table from Rails Documentation

If you look at the row where the Controller#Action are ads#show, ads#update, or ads#destroy, the Path cell is exactly what I was looking for. It seems that the default variable name for the outer resource is the model name, which would be magazine, with a colon appended to the front and “_id” appended to the back. The default variable name for the inner resource is just :id . Paralleling this to my Tenants Controller, the nested path should look like “localhost:3000/landlords/:landlords_id/tenants/:id”.

Also, taking another look at the Controller#Action column, all of the controller actions are based on RESTful conventions by default. So it seems that the controller actions for an un-nested and nested route are the same.

With this new information, if the controller actions are the same, how do I differentiate what the action (or method) executes between a nested and un-nested path? The key to this lies within the path itself.

Nested Path: localhost:3000/landlords/:landlord_id/tenants/:id

Un-nested Path: localhost:3000/tenants/:id

For the nested path, the params hash will contain the keys landlord_id and id . For the un-nested path, the params hash will contain the key id . The landlord_id is how I can differentiate between these two type of routes.

Simultaneously Implementing RESTful Conventions for a Nested and Un-nested Routes

Let’s use show and create as an example for this implementation. As a quick reminder, I am going to copy these two methods from my Tenants Controller from earlier.

def show
id = params[:id]
tenant = Tenant.find(id)
render json: tenant
end
def create
# param keys may subject to change depending on the body of the post request
new_tenant = Tenant.new(first_name: params[:first_name], last_name: params[:last_name])
if new_tenant.valid?
Tenant.create(first_name: params[:first_name], last_name: params[:last_name])
# message can be altered for better UI experience in the future
render json: {message: "Tenant successfully created"}
else
# message can be altered for better UI experience in the future
render json: {message: "There was an error."}
end
end

Currently, the code above assumes that the landlord_id doesn’t exist because its based on the un-nested path. Since we only care if the landlord_id exists or not, an if/else statement would be perfect to use here.

def show
if !params[:landlord_id]
id = params[:id]
tenant = Tenant.find(id)
render json: tenant
else
id = params[:id]
landlord_id = params[:landlord_id]
tenant = Landlord.find(landlord_id).tenants.find(id)
render json: tenant
end
end

In the if portion of the code above, it is exactly the same code as before because the if condition is if the landlord_id doesn’t exist (i.e., the un-nested path). In the else portion, the code is finding the specific tenant under the landlord whose ID is landlord_id .

This is the most basic and simplest example. The create method will be slightly more complicated than the show method.

First, let’s create an if/else statement, just like the show method, and use the same code as before.

def create
if !params[:landlord_id]
# param keys may subject to change depending on the body of the post request
new_tenant = Tenant.new(first_name: params[:first_name], last_name: params[:last_name])
if new_tenant.valid?
Tenant.create(first_name: params[:first_name], last_name: params[:last_name])
# message can be altered for better UI experience in the future
render json: {message: "Tenant successfully created"}
else
# message can be altered for better UI experience in the future
render json: {message: "There was an error."}
end
else
# input new code here for the nested route
end
end

Next we need to think about how to implement creating a new tenant model under an existing landlord. We can use the same logic for the un-nested route because we still need to create a new tenant anyways. However, since we are doing it under an existing landlord, we would need to associate the new tenant model with the existing landlord. Since the tenant and landlord have a many-to-many relationship though reviews, a new review model also needs to be created whose foreign keys are the existing landlord’s primary key and the new tenant’s primary key to create that association.

Below is the code that will accomplish this:

def create
# param keys may subject to change depending on the body of the post request
if !params[:landlord_id]
new_tenant = Tenant.new(first_name: params[:first_name], last_name: params[:last_name])
if new_tenant.valid?
Tenant.create(first_name: params[:first_name], last_name: params[:last_name])
# message can be altered for better UI experience in the future
render json: {message: "Tenant successfully created"}
else
# message can be altered for better UI experience in the future
render json: {message: "There was an error."}
end
else
new_tenant = Tenant.new(first_name: params[:first_name], last_name: params[:last_name])
if new_tenant.valid?
Tenant.create(first_name: params[:first_name], last_name: params[:last_name])
# all inputs for creating the Review are default values. Only the landlord_id and tenant_id are actual correct values. MUST BE UPDATED!!! Needed the default values to pass Review validations
Review.create(start_date: Date.new(2000, 1, 1), end_date: Date.new(2000, 1, 1), landlord_id: params[:landlord_id], tenant_id: Tenant.last.id, address: "TBD", comment: "TBD")
# message can be altered for better UI experience in the future
render json: {message: "Tenant successfully created"}
else
# message can be altered for better UI experience in the future
render json: {message: "There was an error."}
end
end
end

Key Takeaway

Documentation is your friend. It may save you time and headaches depending on your application and implementation of your code.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade