3.1.1 : MVC

Introduction

MVC stands for Model View Controller, these represent 3 logical component of web applications. We use the MVC mental model to refactor our code into multiple files and folders. The MVC concept helps us separate concerns in our web apps but does not strictly define what logic goes in which files, because different web frameworks have slightly different connections.

Example Code for Grocery Application

We will give you the steps to develop an example grocery store application that contains a single model. You can find a repository containing the example application here.

This application is composed of a backend and a frontend, the backend will contain the Model and Controllers while the frontend handles the View.

Here are some of the functionalities of the backend of the application.

  1. Create a consumable GET API that sends a list of the product stored within the database

  2. Create a consumable POST API that allows users to add new products into the database

  3. Create a consumable GET API that retrieves a single product from the database

When using the MVC setup for your application to add a new product into the database through the API, our applications will preform these actions:

  1. The React view captures the users input and sends this data to the controller as a POST request.

  2. The controller (which is based on the server) will alter the current model (database) to insert the new product into the database.

  3. Once the update is completed, the controller retrieves the recently added product and sends it back to the view as a JSON response.

  4. The view then is able to update its internal state such that the most current information represented in the server model will be rendered onto the screen.

Implementing the Grocery Application

Now let's get our hands dirty and start to develop a grocery application that follows the MVC setup. We are going to need to set up a nodeJs project for our application, with this in mind we will also need to implement Sequelize database, complete with a migration, model and seed file. If you want a refresher, please look at this material. You will need to create and alter a .env file so you can protect your sensitive data. Look here if you want to remember how to use the .env. We will also be setting up the configuration slightly differently so please, follow closely.

Setting up the Backend

Firstly, find the place on your machine where you want to develop, and create a directory there. When we are setting up the backend we are developing the Model and Controller of the application. We will be using Reactjs to develop our frontend and View. In this current directory make a new folder named grocery_back to store your backend and cd into it.

Setting up the Database

Before you attempt to setup the database with the sequelize-cli you will need to ensure your Postgresql server is running.

Windows users run these commands:

sudo service postgresql start
sudo su postgres
psql postgres

Macos, make sure your Postgres application is running.

Now you can run these commands to set up your backend:

npm init -y

npm i sequelize pg dotenv

npm i -D sequelize-cli

Next create a new file inside grocery_back named .sequelizerc

The purpose of the .sequelizerc is to configure your application's connection to your database as well as setup the CLI such that files are created in the correct directories. If you would like to see the other configurations that are possible look here.

Make the file appear as below:

const path = require("path");

module.exports = {
  config: path.resolve("config", "database.js"),
  "models-path": path.resolve("db", "models"),
  "seeders-path": path.resolve("db", "seeders"),
  "migrations-path": path.resolve("db", "migrations"),
};

Within the grocery_back directory run the command:

npx sequelize init

The db folder that is generated will contain these folders:

  • config, contains config file, which tells CLI how to connect with database

  • models, contains all models for your project

  • migrations, contains all migration files

  • seeders, contains all seed files

Alter the database.js that is stored within the config folder. You will need to reference the .env that you setup earlier in this file.

The database.js should look like below:

db/config/database.js
require("dotenv").config();

module.exports = {
  development: {
    username: process.env.DB_USERNAME,
    password: process.env.DB_PASSWORD,
    database: process.env.DB_NAME,
    host: process.env.DB_HOST,
    dialect: process.env.DB_DIALECT,
  },
};

Now you can actually create your database, we will need to set up migrations, models and seeds before we can interact with a real database.

At this stage we can create a database within the grocery_back directory run this command:

npx sequelize db:create

Create Database Migrations:

New let's set up our migration file which will be used to create our table in Sequelize. Within the grocery_back directory run this command:

npx sequelize migration:generate --name products

The command above should create a new migration file within the db directory, inside the migration folder, db/migrations/.... Edit the newly generated file so it looks like below:

"use strict";

module.exports = {
  async up(queryInterface, Sequelize) {
    await queryInterface.createTable("products", {
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: Sequelize.INTEGER,
      },
      name: {
        type: Sequelize.STRING,
      },
      price: {
        type: Sequelize.INTEGER,
      },
      created_at: {
        type: Sequelize.DATE,
        allowNull: false,
      },
      updated_at: {
        type: Sequelize.DATE,
        allowNull: false,
      },
    });
  },

  async down(queryInterface, Sequelize) {
    await queryInterface.dropTable("products");
  },
};

After completing this set up we will be able to run our migration file and create a table within our database, within the grocery_back directory run this command:

npx sequelize db:migrate

After running our migration we should develop our product model, which will enable our controller to easily interface with the data stored within the table.

Create Database Model:

Create a file named product.js within the db/models folder.

The product.js file should look similar to below:

db/models/product.js
"use strict";

const { Model } = require("sequelize");
module.exports = (sequelize, DataTypes) => {
  class Product extends Model {}
  Product.init(
    {
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: DataTypes.INTEGER,
      },
      name: {
        type: DataTypes.STRING,
      },
      price: {
        type: DataTypes.INTEGER,
      },
      createdAt: {
        type: DataTypes.DATE,
        allowNull: false,
        defaultValue: new Date(),
      },
      updatedAt: {
        type: DataTypes.DATE,
        allowNull: false,
        defaultValue: new Date(),
      },
    },
    {
      sequelize,
      modelName: "product",
      underscored: true,
    }
  );
  return Product;
};

We will also need to make sure that we have an index.js that will be used to process all of your models and give their Sequelize context to the application.

The index.js will need to be within the models directory that should be implemented as below:

db/models/index.js
"use strict";

const fs = require("fs");
const path = require("path");
const Sequelize = require("sequelize");
const basename = path.basename(__filename);
const env = process.env.NODE_ENV || "development";
const config = require("../../config/database.js")[env];
const db = {};

let sequelize;
if (config.use_env_variable) {
  sequelize = new Sequelize(process.env[config.use_env_variable], config);
} else {
  sequelize = new Sequelize(
    config.database,
    config.username,
    config.password,
    config
  );
}

fs.readdirSync(__dirname)
  .filter((file) => {
    return (
      file.indexOf(".") !== 0 && file !== basename && file.slice(-3) === ".js"
    );
  })
  .forEach((file) => {
    const model = require(path.join(__dirname, file))(
      sequelize,
      Sequelize.DataTypes
    );
    db[model.name] = model;
  });

Object.keys(db).forEach((modelName) => {
  if (db[modelName].associate) {
    db[modelName].associate(db);
  }
});

db.sequelize = sequelize;
db.Sequelize = Sequelize;

module.exports = db;

Now that we have developed our Model, for product we can create our seed file to help populate our database. Within the grocery_back directory run the command below:

Create Database Seeders:

npx sequelize seed:generate --name products

The command above should create a new seed file within the folder db/seeders/, we will use this file to create some sample data for the application.

Edit the newly generated file so that it looks like below:

"use strict";

module.exports = {
  async up(queryInterface, Sequelize) {
    await queryInterface.bulkInsert("products", [
      {
        name: "Doritos",
        price: 15,
        created_at: new Date(),
        updated_at: new Date(),
      },
      {
        name: "Banana",
        price: 10,
        created_at: new Date(),
        updated_at: new Date(),
      },
      {
        name: "Apple",
        price: 10,
        created_at: new Date(),
        updated_at: new Date(),
      },
      {
        name: "Iphone",
        price: 11500,
        created_at: new Date(),
        updated_at: new Date(),
      },
      {
        name: "Cheese",
        price: 50,
        created_at: new Date(),
        updated_at: new Date(),
      },
    ]);
  },

  async down(queryInterface, Sequelize) {
    await queryInterface.bulkDelete("products", null, {});
  },
};

After completing this set up we will be able to run our seeder file and populate our product table within our database, within the grocery_back directory run this command:

npx sequelize db:seed:all

Now that we have setup and populated our database we will need to develop an express server that can interact with it.

Note

The code below will implement classes within our applications. If it looks unfamiliar have a look at the Rocket curriculum to touch up your understanding.

Setting up Routes:

We should develop router files to keep HTTP method and URL path matching outside of the index.js. In this current project we will develop one router file, but generally every router and controller refer to a single type of data stored in your database. In this case we only have a product so only the product router and controller are required.

ProductRouter.js is a file that will bind the controller methods such that they are given the express http request and response context. Meaning we can link an API call to our controller to update our database.

Please create a Routers directory in the grocery_back directory and create a ProductRouter.js file within, it should be similar to the code below:

/Routers/ProductRouter.js
class ProductsRouter {
  constructor(express, controller) {
    this.express = express;
    this.controller = controller;
  }

  routes() {
    const router = this.express.Router();

    router.get("/", this.controller.getAll.bind(this.controller));
    router.get("/:productId", this.controller.getOne.bind(this.controller));
    router.post("/", this.controller.insertOne.bind(this.controller));
    return router;
  }
}

module.exports = ProductsRouter;

Setting Up Controllers:

At this point in development we have setup our database, and express application with routes. Now we need to develop the controller linked up to the API routes defined above. Lets develop some controller methods to handle our requests and send back proper responses.

Each feature or data source can have its own controller. Create a new folder inside the grocery_back directory named Controllers, create two files within this folder, named ProductController.js and BaseController.js.

We will setup the BaseController.js first as we will be creating a class template that can be used for every subsequent controller we need to develop. Please make the file similar to the code below:

/Controllers/BaseController.js
class BaseController {
  constructor(model) {
    this.model = model;
  }

  async getAll(req, res) {
    console.log(this.model);
    try {
      const output = await this.model.findAll();
      return res.json(output);
    } catch (err) {
      console.log(err);
      return res.status(400).json({ error: true, msg: err });
    }
  }
}

module.exports = BaseController;

We can model the ProductsController class on the BaseController class, please implement the file ProductsController.js as below:

/Controllers/ProductsController.js
const BaseController = require("./baseController");

class ProductsController extends BaseController {
  constructor(model) {
    super(model);
  }

  async insertOne(req, res) {
    const { name, price } = req.body;
    try {
      const newProduct = await this.model.create({
        updated_at: new Date(),
        created_at: new Date(),
        name: name,
        price: price,
      });
      return res.json(newProduct);
    } catch (err) {
      return res.status(400).json({ error: true, msg: err });
    }
  }

  async getOne(req, res) {
    const id = req.params.productId;
    try {
      const output = await this.model.findByPk(id);
      return res.json(output);
    } catch (err) {
      console.log(err);
      return res.status(400).json({ error: true, msg: err });
    }
  }
}

module.exports = ProductsController;

Setup Express Js Server:

Ensure that your CLI is within the grocery_back directory and run this command:

npm i express cors

This will install the packages, express and cors, express will be used to power our application and cors is used to facilitate communication between our front and backend.

Lets make a new file within the grocery_back directory named index.js the document should be as below:

index.js
const express = require("express");
const cors = require("cors");
require("dotenv").config();

const db = require("./db/models/index");
const { product } = db;

const ProductsRouter = require("./routers/productsRouter");
const ProductsController = require("./controllers/productsController");

const PORT = process.env.PORT || 3000;

const app = express();

const productsController = new ProductsController(product);
const productsRouter = new ProductsRouter(express, productsController).routes();

app.use(cors());
app.use(express.json());

app.use("/products", productsRouter);

app.listen(PORT, () => {
  console.log("Application listening to port 3000");
});

As you can see from the file above we still need to implement a few files in order to make our MVC application work. We will define a router system as well as a Controller within our express application. We try to reduce the size of the index.js, only initialising what is required and implementing middleware. This file structure enables multiple developers to work concurrently with minimal interference. If we require additional middleware like auth middleware we can import it and bind it to the application within the index.js.

Running Backend Application

Provided the you have installed all of the required dependencies and you have implemented the backend of this application by following the steps above, we should be able to run the application, from the grocery_back directory, run this command:

nodemon index.js

OR (if you don't have nodemon installed

node index.js

You can test your 'Model' and 'Controller' by using ThunderClient, please test out your routes and ensure you can send and retrieve data from your database before moving to the next section. To test out the GET request we need to fire off a request to the URL http://localhost:3000/products, this will respond with a list of all of the products from the backend. It is able to do this because the route handler fires off the getAll method within the controller and returns the data which is sent back to the client, in this case ThunderClient. In order to test out the POST request you need to alter the request within ThunderClient to send a POST request not a GET request. We can do this at the top of the window, with this in mind as the POST request is mocking a form submission we will need to attach the form data you can use a JSON object to achieve this. Remember that the data you add will interface with your database and thus the data's keys need to match column names. When ThunderClient sends this request the insertOne method within the controller is fired off, adding a new fruit into the database, that being said, the response is the newly updated products list.

Model

The Model logical component in the MVC refers to the structure of data in our application, and is the component responsible for manipulating data in the database. In this Coding Bootcamp we will use the Sequelize library to power our model architecture, though it should be noted business logic is tired to the controller. ‘Model’ in MVC refers to the structure of data, as well as how it is stored and queried. Other non-sql database have a variation of Sequelize's model that is used to query tables and data.

View

View refers to application UI. We’ve already defined views in the ‘views’ folder with JS files. MVC distinguishes between “view logic” and “application logic”. View logic determines how data should be rendered and formatted, e.g. transforming data format without changing the underlying value. Application logic determines how data should be calculated and stored. Views typically contain view logic, and controllers typically contain application logic. We are already developing the frontend of our applications, using the Create-React-App, which is our 'View' within the MVC model.

The following are examples of view logic.

  1. Uppercasing a post title

  2. Shortening post content to fit into a table

  3. Transforming a boolean value in the database to a contextual visual element, for example a heart icon for where a user has liked a post.

Controller

Controller refers to the business logic. Controllers are the glue between the model and view, and handle HTTP requests and responses. For example, a controller would determine if, when and how an app would respond with a 404 error message. In Bootcamp, controllers will contain the majority of out applications business logic, and generally everything not a model or view will go into a controller.

Routes

Other than model, views and controllers, we will also develop a router file or files that only connect requests to controllers via the requests’ HTTP method and URL path. This is what we have been doing with methods such as app.get and app.post. We can imagine route files as a directory of our server’s response logic.

Setting up the React Application

The React application will be representative of the view that we are creating with the MVC application setup, it will communicate with the Controller through the backend server that was created earlier.

The frontend React application will consume the API’s served by the expressJS backend. It will make a GET request to retrieve all of the available products currently stored within the database. The application will allow users to send a POST request that will create a new item within the database.

Generating boilerplate with Create-React-App:

To develop the frontend of this application we will be using the Create-React-App. Make sure you are in the root of the project directory and not the grocery_back directory. Run this command:

npx create-react-app grocery_front

This will create a new react application on your machine within the grocery_front directory. We will need to alter files within this directory to implement our View.

Creating your View:

Let's first alter the App.js that is stored within the src directory. Make the file appear as below:

/src/App.js
import logo from "./logo.png";
import "./App.css";
import React from "react";
import AddProduct from "./Components/AddProduct";
import SingleProduct from "./Components/SingleProduct";
import axios from "axios";
import { useState, useEffect } from "react";

export default function App() {
  const [openSingle, setOpenSingle] = useState(false);
  const [products, setProducts] = useState([]);
  const [currentId, setCurrentId] = useState("");

  const getInitialData = async () => {
    let initialAPICall = await axios.get(
      `${process.env.REACT_APP_API_SERVER}/products`
    );
    setProducts(initialAPICall.data);
  };

  useEffect(() => {
    getInitialData();
  }, []);

  const toggleView = (product) => {
    setOpenSingle(!openSingle);
    setCurrentId(product.id);
  };

  const createNewProduct = async (name, price) => {
    let product = {
      name,
      price,
    };
    let response = await axios.post(
      `${process.env.REACT_APP_API_SERVER}/products`,
      product
    );
    let newArray = [...products];
    newArray.push(response.data);
    setProducts(newArray);
  };

  return (
    <div className="App">
      <header className="App-header">
        {openSingle ? (
          <div>
            <SingleProduct toggle={toggleView} id={currentId} />
          </div>
        ) : (
          <div>
            <img src={logo} className="App-logo" alt="logo" />
            <h3>Grocery Store</h3>
            <h6>Products</h6>
            <div className="products-container">
              {products && products.length > 0 ? (
                products.map((product) => (
                  <div
                    className="product"
                    key={product.id}
                    onClick={() => toggleView(product)}
                  >
                    <h4>{product.name}</h4>
                    <h5>${product.price}</h5>
                  </div>
                ))
              ) : (
                <p>Failure</p>
              )}
            </div>
            <AddProduct addProduct={createNewProduct} />
          </div>
        )}
      </header>
    </div>
  );
}

Next create a Components folder within the src directory.

Inside this directory create two files, AddProduct.js and SingleProduct.js

Make the AddProduct.js appear as below:

/src/Components/AddProduct.js
import { useState } from "react";

export default function AddProduct(props) {
  const [name, setName] = useState("");
  const [price, setPrice] = useState(1);

  const submit = () => {
    props.addProduct(name, price);
    setName("");
    setPrice("");
  };

  return (
    <div>
      <h3>Add Product Form</h3>
      <label>Product Name:</label>
      <br />
      <input
        type="text"
        value={name}
        placeholder="Add in product name"
        onChange={(e) => setName(e.target.value)}
      />
      <br />
      <label>Product Price:</label>
      <br />
      <input
        type="number"
        onChange={(e) => setPrice(e.target.value)}
        value={price}
      />
      <br />
      <button onClick={submit}>Add Product</button>
    </div>
  );
}

Now make the SingleProduct.js appear as below:

/src/Components/SingleProduct.js
import React from "react";
import { useState, useEffect } from "react";
import axios from "axios";

export default function SingleProduct(props) {
  const [product, setProduct] = useState({});

  const getProduct = async () => {
    let response = await axios.get(
      `${process.env.REACT_APP_API_SERVER}/products/${props.id}`
    );
    setProduct(response.data);
  };

  useEffect(() => {
    getProduct();
  }, []);

  return (
    <>
      <h1>Single</h1>
      <h2>{product.name}</h2>
      <h3>{product.price}</h3>
      <button onClick={() => props.toggle(product)}>Go Back</button>
    </>
  );
}

We will also need to create a .env file that will be stored within the folder grocery_front. It should appear as below:

.env
REACT_APP_API_SERVER=http://localhost:3000

Running your React Application

At this stage you should be able to run your frontend react application. Run the command within grocery_front directory:

npm start

You probably be prompted to start the server on port 3001, accept and type in 'y' then press enter.

You should see the project open itself within your default browser, you should be able to add a product as well as click into a single product.

Exercise

Implement the project above, once you understand how it works and you've run both the frontend and backend add in some additional features.

On the frontend, in the SingleProduct Component make it so you can edit the selected item's name or price, capture user input and send an API request. In the App.js make it so you can delete an item from the frontend, send an API request to alter the 'model'.

To add additional consumable API's you will also need to set up new methods within the controller as well as new routes in the router.

Fruit Application Controller Creation

Testing Fruit Application with Thunder Client

Please checkout the finished code in this repository, ensure that you're on the sequelize_controller branch. To test out this repo, you will need to setup your .env, install the required dependencies with npm install, then run all migrations and seeders such that you can run the application while its connected to your local database. Then you can run node index.js .

Building Fruit Application Frontend

Please checkout the finished frontend code in this repository, ensure that you're on the react_app branch if you want to test the code on your machine you will need to install the dependencies with the command npm install after the installation you can run the application with npm start. To test it with a backend please checkout this repository, ensure that you're on the cors branch if you want to test the code on your machine you will need to install the dependencies with the command npm install after the installation, then implement you .env. If you've not setup the database previously, run your migrations and seeders and once this is completed you can run the application with node index.js.

Last updated