Mercury: My First Full Stack Project

christinecontreras

Christine Contreras

Posted on October 7, 2021

Mercury: My First Full Stack Project

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
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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.
See projects on load

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.
See individual project

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

Enter fullscreen mode Exit fullscreen mode

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]
         })
       }
     })
 }

Enter fullscreen mode Exit fullscreen mode

Creating a Board On The Front-End

create a board

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
Enter fullscreen mode Exit fullscreen mode
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

Enter fullscreen mode Exit fullscreen mode

Creating a Task On The Front-End

create a task

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.

💖 💪 🙅 🚩
christinecontreras
Christine Contreras

Posted on October 7, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related