Leveraging nested routes in fetch requests

Ian Marshall
4 min readApr 19, 2021

Creating my first application with a Javascript front end and Ruby on Rails backend provided plenty of challenges, from small things like mixing up syntax when switching between languages to bigger decision making, like choosing when to create methods and functions in Javascript and when to use Ruby. My biggest challenge was figuring out how to leverage methods in both languages to create an efficient application. I built my project (a simple chat app called Gabbo) vertically, which meant (in practice) tying all of the pieces together without giving too much thought to more nuanced details like querying exactly which chats I should be rendering, when I should be filtering, and what the most efficient strategies might be along the way.

When refactoring, I learned a lot about how to fetch messages from my database that only pertain to the current user. My application consists of three models: a User class (has many chats), a Chat class (belongs to many users, has many Messages) and a Message class (belongs to a user, belongs to a chat). My refactor focused on three areas of my app: my routes, my Chats controller, and my ChatsApi.js file, which encapsulated the logic that made fetch requests to create and show chats between users.

Originally, when rendering chats I made my fetch request to all chats (‘https://localhost3000/chats), then sorted the chats using Javascript to only display those that contain the user_id of the current user. This made sense while building the application because the current user is only defined on the front end, but if the app were to grow to hundreds or thousands (millions?) of users, this sorting step would become increasingly taxing.

To make fetch requests that only return chats that include the current user, the first step is to have nested routes — from my routes.rb file:

Rails.application.routes.draw do
# root ‘index’
resources :users, only: [:index, :show, :create] do
resources :chats, only: [:index, :show, :create] do
resources :messages, only: [:index, :create]
end
end
end

Nested routes reflect a certain logic — messages belong to chats, and chats belong to users, so it would make sense to navigate to a specific chat in a route like /users/1/chats/2/messages, where the 1 and 2 represent the ids associated with a specific user and a specific chat, respectively.

These routes should (perhaps obviously?) be reflected in your fetch requests. In case you are refactoring your routes rather than beginning with nested routes from scratch, it’s worth noting that changing your routes to be nested will affect all of your fetch requests related to users, chats, and messages. Here’s my getChats() function from my ChatsApi.js file:

static baseUrl = `http://localhost:3000/api/v1/users/`static getChats(){
const user_id = User.setCurrentUser().id // this method returns the current user object
fetch(this.baseUrl+`${user_id}/chats`)
.then(r => r.json())
.then(json => {
json[“data”].forEach(element => {
const chatWindow = new Chat({id: element.id,
sender: element.attributes.sender,
recipient: element.attributes.recipient,
messages: element.attributes.messages})
chatWindow.render();
chatWindow.attachToDom();
Chat.getMessages(chatWindow);
})
})
}

Notice that the baseUrl I define is for users rather than chats. Because chats belong to users, I use interpolation to incorporate the current user’s ID, which means that the JSON returned from the fetch request will only include those chats that feature the current user.

One more important ingredient in making sure these fetch requests work is to white list the user_id in your params — from my ChatsApi.js:

class Api::V1::ChatsController < ApplicationController   def index 
user_id = params[:user_id].to_i
sender_chats = Chat.all.select {
|chat| chat.sender_id == user_id}
recipient_chats = Chat.all.select {
|chat| chat.recipient_id == user_id}
all_chats = (sender_chats+recipient_chats).uniq
options = {
include: [:messages]
}
render json: ChatSerializer.new(all_chats, options)
end
private def chat_params
params.require(:chat).permit(:sender_id, :recipient_id, :user_id)
end
end

Here, the index action converts the user_id to an integer because my setCurrentUser() method returns a string value. Next, this value is passed on to define the sender_chats and recipient_chats arrays, which are combined into an all_chats array.

By restricting the JSON that’s returned from the server to just the chats that include the current user, I greatly reduce the amount of sorting that has to be performed by the front end functionality of my application and allow for separation of concerns — sorting the data is done by the portions of the application that are best sorted for interacting with the database. Even though my application is small and rendering chats is not a particularly taxing or time consuming operation, maintaining this separation of concerns would make a huge difference in performance if the application were to scale to thousands or millions of users.

--

--