Christine Contreras
Posted on October 7, 2021
Last week marked the end of the third phase of Flatiron: Ruby. I was very excited about this phase because it transitioned our learning to backend development. I have experience in front-end development, but back-end development seemed so complex to learn on my own. I was also interested in “putting it all together”. In other words, understanding how a front-end communicates with the back-end, in order to give a user a full web experience.
Architectures I Used For My Back-End
MVC (Model-View-Controller)
Since my back-end is built with Ruby and ActiveRecord, I used MVC (Model View Controller) to build my server. This means a website sends a request to my server, the controller interprets the request and then requests the corresponding information from the models. The models then process and gather the information and send it back to the controller, where it’s then returned to the front-end (view).
RESTful API
I also used RESTful architecture for my API. A Rest API (Representational State Transfer Application Programming Interface) follows a set of rules that help a server and client communicate with each other. It breaks down requests (URLs) and responses (data, in our case JSON) into small actions that can be executed separately from one another. The actions consist of CRUD (Read/Get, Create/Post, Delete, Update) requests.
What I Used In My Project
- React framework for my front-end
- Ruby for my back-end
- ActiveRecord to handle my models and communication with my database
- Rack to create my server
Project Overview
I created a project task management app named Mercury (the god of commerce who was the mediator between gods and mortals. The middleman between people, goods, and messages). This app allows you to organize projects into boards and arrange tasks within each board. .
Mercury Models
Project
- Has many boards
- Has many tasks through boards
Boards
- Belongs to a project
- Has many tasks
Tasks
- Belongs to a board
- Has one project through a board
Project -------- < Board -------- < Task
:title :name :name
:color :project_id :due_date
:favorite :description
:status
:priority
:completed
:board_id
Project Abilities
You can make all CRUD calls for Projects
- CREATE a project
- GET/READ all projects
- GET/READ an individual project
- DELETE a project
- UPDATE a project
The Problem I Ran Into With Projects
When loading the Project Overview page, I only ended up needing the Project information and the tasks for each project to show the project progress bar. I didn’t need to show all boards associated with the project, so I didn’t. However, when you click on a project you do need the boards associated with the project to show all information. So I have to make another request to the server to get all the information needed. In hindsight, I could have sent board information on the first call to my server and then passed that individual project information down into a prop, saving me a call to the back-end.
Rack Implementation
class Application
def call(env)
resp = Rack::Response.new
req = Rack::Request.new(env)
# projects get/read
if req.path.match(/projects/) && req.get? #controller interprets the request given from the front-end
#check if requesting all projects or an individual project
if req.path.split("/projects/").length === 1
# retrieve information from model and send back information to the front-end
return [200, { 'Content-Type' => 'application/json' }, [ {:message => "projects successfully requested", :projects => Project.all}.to_json(:include => :tasks) ]]
else
project = Project.find_by_path(req.path, "/projects/")
return [200, { 'Content-Type' => 'application/json' }, [ {:message => "project successfully requested", :project => project}.to_json(:include => { :boards => {:include => :tasks}}) ]]
end #check if all projects or specific project
end #end projects get request
resp.finish
end
end
Find_by_path was a custom method I added to my models. I wanted to move unnecessary code from my controller and into my models to keep the separation of MVC. The models are supposed to handle and parse the request. All of my models ended up needing this method so I moved it into a module and imported it into each model to DRY up my code.
module InheritMethods
module ClassMethods
def find_by_path(path, URL)
id = path.split(URL).last.to_i
find_by_id(id) #implicit self
end
end
end
require_relative './models_module'
class Project < ActiveRecord::Base
extend InheritMethods::ClassMethods #extend is for class methods
has_many :boards, dependent: :destroy
has_many :tasks, through: :boards
end
Projects On The Front-End
When calling all projects I only wanted all tasks to show since I don’t need board information on my overview page. Task information is used to show project completion percentage.
When you click on an individual task I then make another call to the backend for the specific project to get all of the projects boards and tasks.
Board Abilities
You can make all CRUD calls for Boards
- CREATE a board
- GET/READ all boards
- DELETE a board
- UPDATE a board
The Problem I Ran Into With Boards
At first, when I was creating a new board, I was making a second call to fetch projects after the board was successfully added to my back-end. I wasn’t sending the new instance back to my front-end—just a successful message. I realized I could spare an unnecessary call if I just sent back the new instance after it was successfully posted.
Back-End Implementation
class Application
def call(env)
resp = Rack::Response.new
req = Rack::Request.new(env)
# boards post/create
elsif req.path.match(/boards/) && req.post?
# parse JSON into a readable format for my back-end
hash = JSON.parse(req.body.read)
# check if the project ID passed in exists
project = Project.find_by_id(hash["project_id"])
# if project id was valid move on to creating the new board
if project
board = Board.new(name: hash["name"], project_id: hash["project_id"])
if board.save
return [200, { 'Content-Type' => 'application/json' }, [ {:message => "board successfully created", :board => board}.to_json ]] # send board back to front-end
else
return [422, { 'Content-Type' => 'application/json' }, [ {:error => "board not added. Invalid Data"}.to_json ]]
end #end validation of post
else
return [422, { 'Content-Type' => 'application/json' }, [ {:error => "board not added. Invalid Project Id."}.to_json ]]
end #if: check if project exists
end #end boards post request
resp.finish
end
end
Front-End Implementation
const handleCreateBoard = (newBoard) => {
fetch('http://localhost:9393/boards/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
accept: 'application/json',
},
body: JSON.stringify({
name: newBoard.name,
project_id: projectId,
}),
})
.then((res) => res.json())
.then((data) => {
if (boards.length === 0) {
setBoards([data.board])
} else {
setBoards((prevBoards) => {
return [...prevBoards, data.board]
})
}
})
}
Creating a Board On The Front-End
Task Abilities
You can make all CRUD calls for Tasks
- CREATE a task
- GET/READ all tasks
- DELETE a task
- UPDATE a task
The Problem I Ran Into With Tasks
Tasks had the most information stored in them (name, due date, description, status, priority, completed, board id) and I wanted to make sure that all the information was easily implemented when creating a new task.
I could use a lot of validation on the front-end to make sure the user was inputting in the required information, but it seemed less efficient. Instead, I decided it should be the responsibility of the back-end.
Back-End Implementation
require_relative './models_module'
class Task < ActiveRecord::Base
extend InheritMethods::ClassMethods #extend is for class methods
belongs_to :board
has_one :project, through: :board
def self.create_new_task_with_defaults(hash)
name = hash["name"] ? hash["name"] : "New Task"
status = hash["status"] ? hash["status"] : "Not Started"
priority = hash["priority"] ? hash["priority"] : "Low"
completed = hash["completed"] ? hash["completed"] : false
self.new(
name: name,
due_date: hash["due_date"],
description: hash["description"],
status: status,
priority: priority,
completed: completed,
board_id: hash["board_id"]
)
end
end
class Application
def call(env)
resp = Rack::Response.new
req = Rack::Request.new(env)
# tasks post/create
elsif req.path.match(/tasks/) && req.post?
hash = JSON.parse(req.body.read)
board = Board.find_by_id(hash["board_id"])
if board
task = Task.create_new_task_with_defaults(hash) #custom method
if task.save
return [200, { 'Content-Type' => 'application/json' }, [ {:message => "task successfully created", :task => task}.to_json ]]
else
return [422, { 'Content-Type' => 'application/json' }, [ {:error => "task not added. Invalid Data"}.to_json ]]
end #end validation of post
else
return [422, { 'Content-Type' => 'application/json' }, [ {:error => "task not added. Invalid Board Id."}.to_json ]]
end #if: check if board exists
end #end task post request
resp.finish
end
end
Creating a Task On The Front-End
Final Thoughts
This has been my favorite project so far because it helped me understand how the front-end and back-end communicate with each other. It was also my first ever back-end project and it wasn’t as scary as I thought it would be. It was the unknown that seemed to be the problem rather than the material itself.
I would like to add a sign-in/register form to create users. This would allow users to be added to projects and a project to have my users. I would need to create a joiner table that belongs to a project and belongs to a user, making my models a little more complicated but users are a very real part of websites so I would like to incorporate it into Mercury.
Thank you for going with me on this journey! The next stop is phase 4: Ruby on Rails so stay tuned.
Posted on October 7, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.