What is MVC? (Model, View, Controller)

What is MVC? (Model, View, Controller)

MVC Framework (Model, View, Controller)

What is MVC?

As an engineer, you’ll be working closely with other engineers and their code - which means it will be mutually beneficial if all parties working on a project understand the flow and purpose of the code.

To illustrate what MVC is and why it's important, let's first think about what is actually happening when a user is accessing our site to create a To-Do List.

When a user sends requests to our server, we have an API set up to respond to those requests based on what the client sends. We get to control what happens when our server hears all of those requests. An example of some code we have set up by using Express.js built-in middleware is a GET request on our main route ‘/’ to read the list of items in our users To-Do List.

app.get('/',async (request, response)=>{
    const todoItems = await db.collection('todos').find().toArray()
    const itemsLeft = await db.collection('todos').countDocuments({completed: false})
    response.render('index.ejs', { items: todoItems, left: itemsLeft })
})

In order to do that, our API reaches out to the database (in this case MongoDB), to then read the documents (objects) in a specific collection (our 'todos' collection). With those documents, we send the info we gathered over to our template engine (we're using Embedded JavaScript here) which through our template can then respond to the client with readable HTML.

If we have a form on our site, the client can make a request to submit the form. When our server hears their POST request it will make its way to the database and add a new document in the collection with the information provided from the request. But, we will also need to pair this handle with a response to refresh or render so we can trigger a GET request. This makes sure our user can see the most up-to-date rendering of information we have available in our database.

app.post('/addTodo', (request, response) => {
    db.collection('todos').insertOne({thing: request.body.todoItem, completed: false})
    .then(result => {
        console.log('Todo Added')
        response.redirect('/')
    })
    .catch(error => console.error(error))
})

One way we can update an item to complete in our list is to request to the server using fetch with client-side JavaScript. We can signal that it's a PUT request, then forward it to our database, find the specific document to update its completed property from false to true. After that update is successful, we can respond to our client-side JS that everything worked and to refresh, or reload the page.

Client Side

const item = document.querySelectorAll('.item span')

Array.from(item).forEach((element)=>{
    element.addEventListener('click', markComplete)
})



async function markComplete(){
    const itemText = this.parentNode.childNodes[1].innerText 
    try{
        const response = await fetch('markComplete', {
            method: 'put',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({
                'itemFromJS': itemText 
            })
          })
        const data = await response.json()
        console.log(data)
        location.reload()

    }catch(err){
        console.log(err)
    }
}

Server Side

app.put('/markComplete', (request, response) => {
    db.collection('todos').updateOne({thing: request.body.itemFromJS},{
        $set: {
            completed: true
          }
    },{
        sort: {_id: -1},
        upsert: false
    })
    .then(result => {
        console.log('Marked Complete')
        response.json('Marked Complete')
    })
    .catch(error => console.error(error))
})

To delete an item, we can click an EventListener (in our example below it is a trash icon, fa-trash) which will trigger a fetch request to our server, telling our server we have received a DELETE request. From that request, we can grab what needs to be deleted from the request body so that the code set up on our server can respond by going to our database, finding that document, deleting the item, then telling us that the item has been deleted and that we should refresh the page.

Client Side

const deleteBtn = document.querySelectorAll('.fa-trash')

Array.from(deleteBtn).forEach((element)=>{
    element.addEventListener('click', deleteItem)
})


async function deleteItem(){
    const itemText = this.parentNode.childNodes[1].innerText
    try{
        const response = await fetch('deleteItem', {
            method: 'delete',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({
              'itemFromJS': itemText
            })
          })
        const data = await response.json()
        console.log(data)
        location.reload()

    }catch(err){
        console.log(err)
    }
}

Server Side

app.delete('/deleteItem', (request, response) => {
    db.collection('todos').deleteOne({thing: request.body.itemFromJS})
    .then(result => {
        console.log('Todo Deleted')
        response.json('Todo Deleted')
    })
    .catch(error => console.error(error))

})

There is a lot happening at any given moment just by navigating or using a website. As websites and applications become more complex, the requests users make and the responses designed to handle their requests can grow substantially. Just handling these four simple GET, POST, PUT, and DELETE requests created a lot of bloat on our server.js file.

What if I told you there was a way to structure and organize our files to make our codebase more readable, make adding new functionality a breeze, and using new technologies in your stack extremely straightforward?

wow

MVC is a paradigm that frames our structure for directories and files. This allows us a separation of concerns when it comes to backend development. When practiced, developers can quite literally follow a path of their programs actions from processing a client request all the way to the server's response back to the client.

MVC illustration.JPG

confusion

That's a lot of shapes and lines ...so let's break it down.

We understand that a client makes requests to our server.

On our server, we have code running so requests will be Routed along the appropriate path in order to arrive at their specific Controller.

Controllers determine what responses will be sent back to a user depending on their request. Our Controllers have methods defined to perform different operations such as CRUD, by:

  • Communicating with our Views, which contains our templates for how pages on our website will be viewed by our users.

  • Communicating with our Model, which is set up to interact with our database by entering data OR extracting data. This helps model our data, or standardize how data we handle is put into and taken out of our database.

Let's take a look at how this can be implemented for our To-Do List:

Server

//Require our Modules and the paths we have our Routes set to
const express = require('express')
const app = express()
const mongoose = require('mongoose')
const mainRoutes = require('./routes/main')
const todoRoutes = require('./routes/todos')
//Set up Routes for our Server to listen on
app.use('/', mainRoutes)
app.use('/todos', todoRoutes)

We can see that we're listening our mainRoute & our todosRoute. Let's follow the path we are requiring to find our todos file which is placed in our routes folder

Routes

const express = require('express')
const router = express.Router()
const todosController = require('../controllers/todos') 
//CRUD operations: For parameters we have our route and a method being activated by dot notation within our controller
router.post('/createTodo', todosController.createTodo)
router.put('/markComplete', todosController.markComplete)
router.delete('/deleteTodo', todosController.deleteTodo)
//Export router so we can use it elsewhere 
module.exports = router

We can see the path required by our todosController, so let's follow it to see which methods are running for each request.

Controllers

const Todo = require('../models/Todo')

module.exports = {
    getTodos: async (req,res)=>{
        console.log(req.user)
        try{
            const todoItems = await Todo.find({userId:req.user.id})
            const itemsLeft = await Todo.countDocuments({userId:req.user.id,completed: false})
            res.render('todos.ejs', {todos: todoItems, left: itemsLeft, user: req.user})
        }catch(err){
            console.log(err)
        }
    },
    createTodo: async (req, res)=>{
        try{
            await Todo.create({todo: req.body.todoItem, completed: false, userId: req.user.id})
            console.log('Todo has been added!')
            res.redirect('/todos')
        }catch(err){
            console.log(err)
        }
    },
    markComplete: async (req, res)=>{
        try{
            await Todo.findOneAndUpdate({_id:req.body.todoIdFromJSFile},{
                completed: true
            })
            console.log('Marked Complete')
            res.json('Marked Complete')
        }catch(err){
            console.log(err)
        }
    },
    deleteTodo: async (req, res)=>{
        console.log(req.body.todoIdFromJSFile)
        try{
            await Todo.findOneAndDelete({_id:req.body.todoIdFromJSFile})
            console.log('Deleted Todo')
            res.json('Deleted It')
        }catch(err){
            console.log(err)
        }
    }
}

We can see that we're performing the same type of async functions in our controller that we were previously, but they're aggregated here for easy viewing. In addition, our client-side JavaScript still looks the same with either our async / await or .then / .catch& fetch functions.

We can also see that we're requiring our Todo Models - a variable we are utilizing frequently within our individual methods. Let's follow along the path to our Todo Model

Models

const mongoose = require('mongoose')

const TodoSchema = new mongoose.Schema({
  todo: {
    type: String,
    required: true,
  },
  completed: {
    type: Boolean,
    required: true,
  },
  userId: {
    type: String,
    required: true
  }
})

module.exports = mongoose.model('Todo', TodoSchema)

Here we are using Mongoose Schemas to structure data we are sending to our database.

What is Mongoose / Schemas?

Mongoose is an Object Data Modeling library available via npm install that bridges MongoDB and Node.js with the use of Schemas. Through Schemas, we can:

  • Define a structure of documents (What properties and value types are we looking for? Should there be default values?)
  • Pass our Schema into a usable Model that can interact with our Database (Which documents do we want to Create, Read, Update or Delete?)
  • Assign IDs to properties in our Schema (Unique identifiers we can reference when manipulating our data)

This, however, means that if we want to communicate with our database via Mongoose, we need to follow the constraints of our Schema - if we make any changes to our Schema our mapping relationship changes entirely, or if fields with improper properties or value types are entered they will not be inserted into our database. Mongoose enforces Schema Validation at the application layer - in other words, what can be entered as a property and value types have already been declared.

Why use MVC?

MVC is a framework that follows a paradigm of abstraction - we as developers can separate actions carried out by our websites or applications server. This allows us an organized structure that we can follow along, like a path, to what is happening throughout our application.

The framework itself as we saw is not a requirement, but a luxury - developers can slab all of these processes right onto their server file, as we initially did, and their code will still operate the same. When building and adding new functionality, engineers that created those specific connections and responses may remember where to look. However, people unfamiliar with the codebase whether they are non-technical users or new engineers, perhaps even the original engineer revisiting code after some months or years may still have to scour the entire jumbled server file just to understand the connections within. In this regard, using MVC will help others understand what's happening - allowing them to follow a clear path of thought. Ultimately this can save employees time and headaches, as well as saving employers person-hours and money.

If we ever want to swap out our template engine or even the database in use, we can do it easily at any time with MVC. We do not have to worry about impeding upon the functionality of the rest of our program because we separated our Models, Views, and Controllers. If we wished to use React and JSX instead of EJS - we simply swap out our views and connect to our Controllers. If we wanted to swap out our NoSQL database in MongoDB with a relational database such as PostgreSQL, we only need to redevelop our Model.

For these reasons, MVC may be worth implementing in some of your projects and at the very least is worth knowing about as you continue your journey as a Software Engineer.