Bootcamp
Search…
4.2: MVC

Introduction

MVC stands for "Model View Controller" and refers to 3 logical components of web applications. We will 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 conventions.

Model

The Model logical component in MVC refers to the structure of data in our applications, and is the component responsible for manipulating data in the database. In Coding Bootcamp we will use the Sequelize library to power our model architecture, but "model" in MVC refers to structure of data in general and is not dependent on Sequelize, any library, or even a SQL database.
Depending on the opinion of the architect, more or less logic can be placed in models. Read more about that here. In Coding Bootcamp we will use models that only contain our Sequelize model definitions, and relegate business logic to our Controllers.

View

View refers to application UI. We've already defined views in the views folder with EJS 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 underlying value. Application logic determines how data should be calculated and stored. Views typically contain view logic, and controllers typically contain application logic.
The following are examples of view logic.
  1. 1.
    Uppercasing a post title
  2. 2.
    Shortening post content to fit in a table
  3. 3.
    Transforming a boolean value in the DB to a contextual visual element, for example a heart icon for whether a user has liked a post.

Controller

Controller refers to business logic. Controllers are the glue between model and views, and handle HTTP requests and responses. For example, a controIler would determine if, when, and how an app would respond with a 404. In Coding Bootcamp, controllers will contain the majority of our applications' business logic, and generally everything not a model or view will go in a controller. Other web frameworks such as Ruby on Rails advocate for "fat models and skinny controllers", where business logic can be abstracted to models to keep controller logic clean. This is a matter of preference.

Routes

Other than models, views, and controllers, we will also have a route 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.

Grocery App Example

We'll create an example grocery app with a single model.
  1. 1.
    A request comes into the controller.
  2. 2.
    The controller calls the model in the callback.
  3. 3.
    The result of the model call is passed to the view. The view uses the data to render.
  4. 4.
    response.render sends the rendered view back in the response.

Setup Packages and Folders, Configure DB

Set up Sequelize with a new Node application and configure the DB in the same way we did in Module 4.1.1: Intro to Sequelize. Update config.js to use a new DB name, grocerymvc_development instead of grocery_development to distinguish the DB from modules 4.2.1's. Stop after creating the DB and follow the steps below to create an app with MVC architecture.

Migrations: Create Items Table

Generate Migration

1
npx sequelize migration:generate --name create-items-table
Copied!
Delete the entire contents of the file and write the table creation code:

<GENERATED_DATE>-create-items-table.js

1
module.exports = {
2
up: async (queryInterface, Sequelize) => {
3
await queryInterface.createTable("items", {
4
id: {
5
allowNull: false,
6
autoIncrement: true,
7
primaryKey: true,
8
type: Sequelize.INTEGER,
9
},
10
name: {
11
type: Sequelize.STRING,
12
},
13
created_at: {
14
allowNull: false,
15
type: Sequelize.DATE,
16
},
17
updated_at: {
18
allowNull: false,
19
type: Sequelize.DATE,
20
},
21
});
22
},
23
​
24
down: async (queryInterface, Sequelize) => {
25
await queryInterface.dropTable("items");
26
},
27
};
Copied!

Run Migration

1
npx sequelize db:migrate
Copied!

Verify Migration

1
psql -d grocerymvc_development
Copied!

Models: Create and Initialise Item Model

models/item.mjs

1
export default function initItemModel(sequelize, DataTypes) {
2
return sequelize.define(
3
"item",
4
{
5
id: {
6
allowNull: false,
7
autoIncrement: true,
8
primaryKey: true,
9
type: DataTypes.INTEGER,
10
},
11
name: {
12
type: DataTypes.STRING,
13
},
14
createdAt: {
15
allowNull: false,
16
type: DataTypes.DATE,
17
},
18
updatedAt: {
19
allowNull: false,
20
type: DataTypes.DATE,
21
},
22
},
23
{
24
// The underscored option makes Sequelize reference snake_case names in the DB.
25
underscored: true,
26
}
27
);
28
}
Copied!

models/index.mjs

1
import { Sequelize } from "sequelize";
2
import allConfig from "../config/config.js";
3
​
4
import initItemModel from "./item.mjs";
5
​
6
const env = process.env.NODE_ENV || "development";
7
​
8
const config = allConfig[env];
9
​
10
const db = {};
11
​
12
let sequelize = new Sequelize(
13
config.database,
14
config.username,
15
config.password,
16
config
17
);
18
​
19
db.Item = initItemModel(sequelize, Sequelize.DataTypes);
20
​
21
db.sequelize = sequelize;
22
db.Sequelize = Sequelize;
23
​
24
export default db;
Copied!

Seeders: Create Sample Items

1
npx sequelize seed:generate --name seed-data
Copied!

<GENERATED_DATE>-seed-data.js

1
module.exports = {
2
up: async (queryInterface) => {
3
const itemsList = [
4
{
5
name: "doritos",
6
created_at: new Date(),
7
updated_at: new Date(),
8
},
9
{
10
name: "mangoes",
11
created_at: new Date(),
12
updated_at: new Date(),
13
},
14
{
15
name: "pork shoulder",
16
created_at: new Date(),
17
updated_at: new Date(),
18
},
19
];
20
await queryInterface.bulkInsert("items", itemsList);
21
},
22
​
23
down: async (queryInterface) => {
24
await queryInterface.bulkDelete("items", null, {});
25
},
26
};
Copied!

Run Seed Migration

1
npx sequelize db:seed:all
Copied!

Express: Incorporate Sequelize in Express

Install Packages and Create Folders

Install Express.js and all the standard libraries:
1
npm install express ejs method-override cookie-parser
Copied!
Create standard Express.js app directories views and public.
1
mkdir views public
Copied!

index.mjs

The following is a standard Express.js root file setup.
1
import express from "express";
2
import cookieParser from "cookie-parser";
3
import methodOverride from "method-override";
4
​
5
import bindRoutes from "./routes.mjs";
6
​
7
// Initialise Express instance
8
const app = express();
9
// Set the Express view engine to expect EJS templates
10
app.set("view engine", "ejs");
11
// Bind cookie parser middleware to parse cookies in requests
12
app.use(cookieParser());
13
// Bind Express middleware to parse request bodies for POST requests
14
app.use(express.urlencoded({ extended: false }));
15
// Bind method override middleware to parse PUT and DELETE requests sent as POST requests
16
app.use(methodOverride("_method"));
17
// Expose the files stored in the public folder
18
app.use(express.static("public"));
19
​
20
// Bind route definitions to the Express application
21
bindRoutes(app);
22
​
23
// Set Express to listen on the given port
24
const PORT = process.env.PORT || 3004;
25
app.listen(PORT);
Copied!
Note that most of the app is now run from line 21. We'll define the routes in the following section. We try to keep the Express root file as small as possible, only adding library configuration middleware. This is so that multiple developers can work on the application with minimal interference with one other. If we need to add custom middleware like user auth middleware we can define it in a controller, import and bind it to app here.

Routes

Create a routes file only for HTTP method and URL path matching. As our apps get more complex we can split our routes into multiple files, but for now let's keep them all in 1 file for simplicity.
Since this is the file in which we invoke controller methods, we will link our controllers to our models in this file. The db instance, containing the connection pool, will be passed around so that every controller has access to the database.
Note the index key of itemsController. We will define this in the controller file below.

routes.mjs

1
import db from "./models/index.mjs";
2
​
3
// import the controller
4
import initItemsController from "./controllers/items.mjs";
5
​
6
export default function bindRoutes(app) {
7
// pass in the db for all items callbacks
8
const itemsController = initItemsController(db);
9
​
10
app.get("/items", itemsController.index);
11
}
Copied!

Controllers

We have written the routes matching requests to controllers in our routes file. Let's write the controller methods that handle the requests. Create a controllers folder to store controllers.
Each feature can have its own controller. In Coding Bootcamp we will export a function (initItemsController in this case) from each controller, such that the parameter to the function db can be used by all methods within this controller without explicitly passing db every time we invoke a controller method.

controllers/items.mjs

1
// db is an argument to this function so
2
// that we can make db queries inside
3
export default function initItemsController(db) {
4
const index = (request, response) => {
5
db.Item.findAll()
6
.then((items) => {
7
response.render("items/index", { items });
8
})
9
.catch((error) => console.log(error));
10
};
11
​
12
// return all methods we define in an object
13
// refer to the routes file above to see this used
14
return {
15
index,
16
};
17
}
Copied!

Controller Template

This example assumes you need to display a list of data. For convenience and consistency we can give standard names to the CRUD methods of our controllers. For example, an index method might retrieve all instances of a model. See the names table below for a complete listing.

controllers/<NAME_LOWER_CAMEL_CASE_PLURAL>.mjs

1
export default function init<NAME_LOWER_CAMEL_CASE_PLURAL>Controller(db) {
2
​
3
// route to render a list of all the <NAME>
4
const index = (request, response) => {
5
db.<MODEL_NAME_UPPER_CAMEL_CASE>.findAll()
6
.then((<NAME_LOWER_CAMEL_CASE_PLURAL>) => {
7
response.render('<NAME_LOWER_CAMEL_CASE_PLURAL>/all', {
8
<NAME_LOWER_CAMEL_CASE_PLURAL>
9
});
10
})
11
.catch((error) => console.log(error));
12
};
13
​
14
return {
15
index
16
};
17
}
Copied!

Views

Create one view folder for each controller and name it after the controller. In this case we name our view folder items.
1
mkdir views/items
Copied!

views/items/index.ejs

1
<% items.forEach(item => { %>
2
<p>
3
<%= item.id %>: <%= item.name %>
4
</p>
5
<% }) %>
Copied!

Run Full Application

Run the server we just created with the following command.
1
node index.mjs
Copied!
Then access the route we defined on our server in the browser via localhost:3004/items.

Naming Conventions: URL Path, Controller Method, View, and Model

Please use the following naming conventions for CRUD MVC components in Coding Bootcamp applications.
URL Path
Method
Purpose
Controller Method Name
View File Name
Sequelize Model Method Name
/items/new
GET
Render a form that will create a new item.
newForm
newForm
N/A
/items
POST
Accept a POST request to create a new item.
create
N/A
create
/items/:id
GET
Render a single item.
show
show
findOne
/items
GET
Render a list of items.
index
index
findAll
/items/:id/edit
GET
Render a form to edit a item.
editForm
editForm
N/A
/items/:id
PUT
Accept a request to edit a single item
update
update
update
/items/:id
DELETE
Accept a request to delete an item.
delete
delete
destroy

Exercise

Replicate the above code and verify results.