The MVC
(Model View Controller) is a common design pattern used
in software development. MVC
separates the Model, View, and Controller code in order to
all us to create more modular and robust applications.
The Model refers to the data models used by the application
(objects, classes, collections); the View refers to the
user interface parts of your application that allow a user
to interact with the program; the Controller contains the
logic that performs the behind-the-scenes work, allowing the
views to update and reflect changes to the model, performing
tasks the user triggers on the view, etc.
In Node.js and Express, it's easiser to design more complex
applications using MVC.
You can phyiscally and logically separate code for your HTML
pages, routing and request processing, and your data.
In Node.js/Express application, we separate the Model, View,
and Controller code as follows:
Model: JSON data/objects, a database schema, a class
that models (represents) various objects or a collection of
objects. Models are stored in a /models sub-directory of the
project. Examples:
A program that sells socks might have a class that models
Sock objects, with properties like colour, type, pattern, material,
and size.
A program that allows users to see the statistics for a sports
team might have a collection of players, and when the user
selects a player, a Player object with all that player's statistics
can be used to update the view (web page).
View: HTML pages where a user can enter data and view
data, where the user can interact with the application. Views
can be templates: these contain placeholders where dynamic
data can go. Views are stored in a /views sub-directory of the
project. Examples:
A web page where the user can browse the list of available
socks to purchase, the shopping cart page where they can
view or edit their cart and see how much it's going to cost before
checking out.
A page with an HTML table that allows a user to see a
player on a sports team with all of their current statistics.
The table contains placeholders for the player name, jersey
number, and all the statistics values. These placeholders are
assigned data by the controller when the request for the page
is being processed.
Controller: the code and logic that routes requests,
processes requests and builds/sends responses, behind-the-scenes
code that looks up data in collections/databases or creates
instances of objects based on user inputs. Controllers are stored
in a /controllers sub-directory of the project. Examples:
The server receives a request to add a specific pair of
socks to the user's cart: it routes the request to a function
that looks up the sock ID in the database and returns all that
item's data as a Sock object, passes that object to a function
that gets a view to display the sock information: the view
cotnains placeholders for all the sock data, so those placeholders
are replaced with all the sock object's property values.
This is then passed to a function that adds the view to the
response, sets the response headers, and then sends the response
back to the client.
The user requests the stats page for a player they
selected in a list: the program routes that request to
a function that looks up the player in a database and returns
a Player object, then a function that takes the Player object
and adds all the player's property values to placeholders in
a view, then builds a response and adds the completed view
to the response body, then sends the response.
Templates
Templates are views that contain placeholders. When a template
page is going to be sent in a response, the placeholders are
filled with actual data. In an Express application, this is
done with the help of a template engine.
A template engine looks for placeholders using its preferred
syntax, and helps your code replace the placeholders with
actual data.
Express supports a few different template engines.
I'm only going to talk about EJS (Embedded JavaScript) template
engine, but you might be interested in learning about
some of the others:
EJS is very simple
and easy to learn: it's just JavaScript enclosed within
<% and %> symbols for the most part. You just code your
regular HTML, but place the code inside files with the
.ejs extension and store them in the project's
/views directory.
EJS commands are enclosed inside <% %> symbols as
indicated below:
<% %> tags contain scriptlets.
Scriptlets are small bits of JavaScript code that contain
loops, if statements, etc. Scriptlets do not produce output,
but can save output values to variables to be used inside
other EJS placeholders. For example:
<%= %> tags are output tags that escape any special
HTML characters (e.g. < for <). These simply
replace themselves with whatever the variable or expression
is inside the tag. For example, if a variable foo
contains the value "bar", then <h1>Welcome, <%=foo%>!</h1>
will render as <h1>Welcome, bar!</h1>
<%- %> is similar to <%= %> except that it
doesn't escape any special HTML characters.
<%# %> is a comment tag, used for documentation
and comments. These do not actually render anything and produce
no output.
There are some other tags you can read about in the EJS
documentation.
EJS will need to be added to a project as part of the npm install
command.
Creating an Express MVC Application
Let's create a very basic MVC application with Express.
Start a new project. I'm calling mine /express_mvc
Add sub-directories for /views and /controllers. We
won't be adding a model, yet. We'll do that in the next
example.
In the /views directory, add a file index.ejs.
This will be an HTML file, but it must have the .ejs extension.
You can copy and paste the following code into your
index.ejs file:
In the HTML, you'll notice inside the level-1 heading is
an EJS output tag:
<h2>Menu for <%=day%></h2>
The <%=day%> is a placeholder: the EJS engine
will search for a variable or parameter called day,
and it will replace <%=day%> with the value of the
day variable.
Now we have to figure out how to pass the value for the
day variable into the index.ejs template.
URL Parameters
One way we can pass a value to our application is by
using a form input (such as a select box for the days of the
week). However, that's not the only way! You can also pass
values into an application by using a URL parameter.
A URL parameter is a segment added to the URL, and the value
of that segment is the value you want to pass to your
program. For example, someone could load our application
with the URL localhost:3000/menu/Tuesday or
localhost:3000/menu/Saturday. We can grab the last segment
as a parameter and pass it to the index.ejs template.
To get the value of the URL parameter, you add a segment
to your app.use()/get()/post() URL pattern, such as
/menu/:day. Notice that we use a colon :
in front of the parameter's name. This adds the parameter
as part of the request: request parameters are stored in the
req.params object. The req.params
object contains properties for any params in the URL,
e.g. req.params.day.
The statement app.set("view engine", "ejs");
sets the view engine to the EJS view engine. By default
it's set to PUG, but we are using EJS so we have to add
this statement to change it to EJS.
Now add a function to print the day URL
parameter to the console:
Notice the URL pattern: /menu/:day. So this matches
any URL that starts with /menu/ and has a value after it.
Right now we'll assume it's a day of the week.
Inside the callback, we're just printing the req.params.day
value to the console. This means we're not sending back a
response, yet - so your browser will show an error when you run the
app, and that's fine for now.
Initialize your app, then run npm install to install
express and ejs:
npm install express ejs -s
Now you can run your program. Go to your browser and try:
localhost:3000/menu/Tuesday
Try different days (you can actually use any string in place
of "Tuesday". You should see your last URL segment in the
console.
Now let's finish our application so that the day of the week
actually shows up in the browser instead of the console.
This requires a controller.
Adding a Controller
The controller's job is to manage the processing
behind the scenes. In this case, our controller will
retrieve the day value from the request parameters and
send it to the index.ejs file, which will then be
sent in the response.
In your /controllers directory, add the file homeController.js.
It's common to have multiple controllers, and most developers
name the main controller either homeController.js
or mainController.js.
Add the strict mode statement to your controller.
The controller is a module, so we want to expose a function
called renderMenu
that can get the parameter value from the request object
and pass it to the view index.ejs. Let's expose our
function by assigning it to exports:
exports.renderMenu = (req, res) => {
};
This module function needs the request and response object, so
they have been added as paramters. It needs the request object
so that it can retrieve req.params.day and it
needs the response object so it can render the index.ejs page
containing teh day value and send that as a
response.
First, let's add the statement that retrieves the
day parameter:
const paramDay = req.params.day;
To add the day value to the index.ejs template, we use the
res.render() function. res.render()
accepts two arguments:
The name of the view to render. This should be a string
value, and the extension is not required (e.g. "index" for
index.ejs)
A JSON object referred to as "locals" that contains
key-value pairs. These key-value pairs end up as variables/values
that are local to your view (hence the term "locals").
res.render() will also automatically set the
response status code, content type, and will write the final
rendered version of the template .ejs file to the response body,
then it will send the response for you! It does a lot of the
work we usually have to do ourselves. This means we can add
one more statement to our controller to complete it:
res.render("index", {day: paramDay});
This takes the value of our paramDay variable and
assigns it to the "day" key, which makes "day" a local variable
in the index.ejs file. This is essentially how you can pass
a value from your request to your controller to your EJS template
response.
Now we need to update our app.get(): the callback should now
be the controller function we just exposed:
First, we need to require() the controller in our app.js:
Now we can update our app.get(): the callback is now
going to be the homeController's renderMenu() function instead
of the custom callback we had previously:
app.get("/menu/:day", homeController.renderMenu);
Now run your program if it's not already running, and try
the URL again with any day of the week:
localhost:3000/menu/Saturday
(or whatever day you like)
Output for localhost:3000/menu/Saturday
Adding a Model
Now let's make it a true MVC program by adding a model.
Grab the menu.json file from GitHub and add it to your
project inside the /models directory.
Examine the JSON file: you'll see it's a list of objects
where each object's key is the day of the week in title case
(e.g. Tuesday, Wednesday, etc). Each object is a simple
menu for three meals: breakfast, lunch, and dinner.
When the user visits a URL for a specific day of the week,
we will display a level-3 heading for each meal with a list
of the items, as shown in the screen shot below:
Proposed output for our MVC program
Our controller's renderMenu() function needs to know which
menu object to add to the list of locals for the index.ejs file.
To do this, we must first import the menu.json file into our
controller:
const menu = require("../models/menu.json");
Note the path: the controller is in the /express_mvc/controllers directory
but the menu.json is in the /express_mvc/models directory, so
we have to go up one level to /express_mvc, then into /models
in order to get the menu.json file.
Now we can update the renderMenu() function by getting
the menu object for the specific day. The menu
variable contains the list of objects, keyed by day of the
week in title case. So we can use req.params.day
as the index to menu:
let paramMenu = menu[req.params.day];
Then we have to add the paramMenu's value to a "menu" parameter
to be sent to the index page as a local variable:
All that's left is to update the index.ejs view. Add the
code in the <main> to iterate through the menu
object keys and print a level-3 heading for each property
(breakfast/lunch/dinner) and a paragraph containing the value
for each property:
<% for (let m in menu) { %>
<h3><%=m%></h3>
<p><%=menu[m]%></p>
<% } %>
Now re-run your program and try each day of the week (make sure
you use title case). What happens if you don't enter a valid
day of the week?
Express MVC with a Form
Let's try an example with a form. In a previous lesson
you wrote a program that displayed the Chinese Zodiac
animal for a specific year. Let's rewrite this as an MVC
program using Express.
Add a new project. I'm calling mine express_zodiac.
Add the /views and /controllers directory. We don't need
a model for this project.
Our form is going to ask for a user's name and the
year. So we'll need to handle a request to load the
index page.
The form on the index page
When the user submits the form, we want to see the following
output:
The output of the program
Once the user submits the form with the two inputs,
we'll need that request to be handled as a POST
request. This request will have a few steps:
Get the request body as url-encoded data.
Retrieve the animal for the user-entered year.
Render the result.ejs with the user's name and the
zodiac year.
Each of these will be its own middleware function.
For this project, we should also separate our code into
two controllers. One homeController, which manages the rendering
of the EJS templates, and one animalController, which
uses the third-party chinese-year package to get the zodiac
animal for a specific year. This allows us to re-use the
animalController in other programs!
Add the two controllers to the /controllers directory:
homeController.js and animalController.js
or call them whatever makes sense to you (although I do thing the
name "animalController" is kind of funny :D )
Animal Controller
The animalController will have one exposed function that
does pretty much what we had in our previous zodiac program:
get the year, make sure if the year is empty that we assign
the current year, then use the year in the getAnimal() endpoint
of the chinese-year package.
To make the animal available to the next middleware function,
we can add it as a parameter in the request body's
query string: that's
a common way of passing data from one middleware function
to another, and it's totally fine: we'll just add a new
item to the request body for the animal we found.
Also remember that in this case, it's from a POST
request, so the query string is accessed via
req.body, not req.query.
"use strict";
// /controllers/animalController.js
const chineseYear = require("chinese-year");
exports.getAnimal = (req, res, next) => {
// make sure the year input has an actual year in it
if (!req.body.year) {
req.body.year = new Date().getFullYear();
}
// get the animal for the year
const animal = chineseYear.getAnimal(req.body.year);
// add the animal to the query string
req.body.animal = animal;
// next middleware
next();
}
Home Controller
Our homeController handles the rendering of the index
page and also the results page when the form has been submitted.
So this controller needs two functions exposed: one to render the
index page and one to render the results page.
The index page function is simple, since it has no
placeholders:
Now we can put it all together in the app.js file. Add the app.js
to your project root if you haven't already done so.
app.js
Add the code to app.js to:
Use strict mode
import express, the animalController, and homeController
set the port number environment variable
set the view engine to "ejs"
start the server
"use strict";
const express = require("express"),
app = express(),
animalController = require("./controllers/animalController"),
homeController = require("./controllers/homeController");
app.set("port", process.env.PORT || 3000);
app.set("view engine", "ejs");
// rest of code will go here
app.listen(app.get("port"), () => {
console.log(`Server running on http://localhost:${app.get("port")}`);
});
Recall that we need a function to handle requests to
the index page. Let's use /zodiac/index, but feel
free to be a bit more creative (e.g. with or without .html)
// route get requests to index page
app.get("/zodiac/index", homeController.renderIndex);
As we discussed, we need three middleware functions to
handle the POST request for form submission:
First, populate req.body with the query string
in the request body:
When the user clicks the submit button, the
page should reload with the circle information in
the first <section> element.
The form asks for a radius
Add the circle.js object/class as the model for your
application (e.g. it should go in /models). Your circle
should have a radius property, two accessor properties
circumference and area, and a toString()
function property. If the radius is not provided when someone
creates a new circle object, use a default value of 0.
Add a single controller with three exposed functions:
renderIndex() renders the index page.
getCircle() uses the inputted radius from the query
string and uses it to contruct a new Circle object.
If radius is empty, construct a default circle.
renderResult() passes the circle to the index.ejs
page and renders it.
Modify your index.ejs page: Inside the empty first
<section> element, add the code to do the following
if a circle object exists as a local:
render a level-2 heading with the content "Circle Information".
below the heading, display the circle (it should automatically
use the toString() if you coded it correctly) in a paragraph
element.
below the circle object, display a paragraph element
containing "Circumference: " followed by the circle circumference
below the circle object, display a paragraph element
containing "Area: " followed by the circle area
TIP: when you render the index page, pass in a null circle
object.