This presentation can be viewed here
<style style="visibility:hidden"> .reveal li {font-size: 32px;} .reveal ul ul li, .reveal ul ol li, .reveal ol ol li, .reveal ol ul li { font-size: 28px; } </style>- Students Will Be Able To:
- Use the Express Generator to Scaffold a Skeleton App
- Implement Best Practice Routing
- Organize App Logic Into Controllers
- Setup
- Express Generator
- MVC Code Organization
- Best Practice Routing
- To-Do Refactor
- Controllers
- MVC Organization Revisited
- URL/Route Parameters
- Adding Show a To-Do Functionality
- To get ready for this lesson, simply
cd
into today's folder for the class repo.
-
Okay, so we've had big fun getting an Express app up and running from scratch.
-
We defined some basic routes and rendered a couple of views using the EJS view engine.
-
Later today we're going to learn about middleware and see how we can create and update data.
-
In this lesson, the first thing we'll take a look at is a popular tool -
express-generator
. -
express-generator
creates a "skeleton" Express app that:- Separates the HTTP server code from our web app's logic.
- Has best practice routing implemented.
- Is configured to serve static assets from a
public
folder. - If we specify it, will configure the EJS view engine.
- Has error handling configured.
- Has key middleware configured and mounted by default.
-
Let's install it:
$ npm install -g express-generator
-
express-generator
is a CLI that can be run from anywhere, that's why we install it using the global-g
flag.
Let's take a look at the options available to us
$ express -h
Usage: express [options] [dir]
Options:
--version output the version number
-e, --ejs add ejs engine support
--pug add pug engine support
--hbs add handlebars engine support
-H, --hogan add hogan.js engine support
-v, --view <engine> add view <engine> support (dust|ejs|hbs|hjs|jade|pug|twig|vash) (defaults to jade)
--no-view use static html instead of view engine
-c, --css <engine> add stylesheet <engine> support (less|stylus|compass|sass) (defaults to plain css)
--git add .gitignore
-f, --force force on non-empty directory
-h, --help output usage information
-
We will use the
-e
option to use the ejs template engine instead of pug (the default). -
From your new app's parent directory:
$ express -e express-todos $ cd express-todos
-
We then install the node modules that are listed in the
package.json
:$ npm i
-
Our scaffolded folder structure will look like this:
├── app.js ├── bin │ └── www ├── package.json ├── public │ ├── images │ ├── javascripts │ └── stylesheets │ └── style.css ├── routes │ ├── index.js │ └── users.js └── views ├── error.js └── index.js
-
Let's explore the above structure in our text editor...
-
One option to start the server is to type
npm start
. This will execute the start script specified in package.json. However, it doesn't restart the app when there's changes... -
nodemon
is still our best option and we can now just typenodemon
which will use that samestart
script.
-
MERN/MEAN Stack apps often have a client-side file named
app.js
and this could get confusing having twoapp.js
files. This is why many developers name their main Express fileserver.js
. -
So let's rename it...
-
First, rename
app.js
toserver.js
. -
Then, inside of
bin/www
, change line 7 from:var app = require('../app');
to:
var app = require('../server');
-
That's it, however, you might need to restart
nodemon
.
-
Model-View-Controller (MVC) has been a proven approach for successfully organizing code for decades.
-
In fact, many web frameworks such as Ruby on Rails, ASP.net, Spring MVC (Java), and others implement the MVC architectural pattern.
-
Express on the other hand, just like it states on its landing page, is unopinionated. That means we are free to structure and organize our Express apps anyway we please.
-
However, since MVC is a proven pattern that works, most Express developers use MVC to organize their Express applications - so we will too :)
-
Express generator has already organized the view templates into a
views
folder. -
Let's make folders to hold our models and controllers:
$ mkdir models controllers
-
In our
first-express
app, we used theapp.get
method to define routes. -
Although it works, the better practice is to:
- Use Express
router
objects to organize related routes, for example, routes dedicated to a data resource such astodos
. - Create each
router
in its own module from which it is exported. require
the exportedrouter
inside of server.js.- Mount the
router
object in the request pipeline (route
objects are also middleware functions).
- Use Express
-
The
Router
objects can provide more flexible and powerful routing in complex apps. -
Router
objects are actually mini-Express apps! They can even have their own middleware.
-
As an example of using this better approach to routing, let's look at how
express-generator
sets up routing... -
First, there's a
routes
folder containing two router modules:- index.js: Great for defining general purpose routes, e.g., the root route.
- users.js: An example of a router dedicated to a resource, in this case, users.
-
Note how routes are defined on those two
router
objects using aget
method just like we did withapp
:router.get()
instead ofapp.get()
-
Each
router
object has one route defined - compare those two routes, notice the method and the paths? They're the same - isn't that a problem? Not in this case...
-
The two route modules are required on lines 7 & 8 of
server.js
. -
Lastly, the routers are mounted in the middleware pipeline with the
app.use
method on lines 22 & 23:app.use('/', indexRouter); app.use('/users', usersRouter);
-
It's important to realize that the path in
app.use
is combined with the path specified on the router objects...
-
Let's say you have a
router
object that defines a route like this:router.get('/', function(req, res) {...
and mounted like this:
app.use('/todos', todoRouter);
What is the actual path of the route?
-
Another example, let's say you have a
router
object that defines a route like this:router.get('/today', function(req, res) {...
and mounted like this:
app.use('/calendar', calendarRouter);
What is the actual path of that route?
-
We're going to refactor the To-Do code from yesterday to follow best practices...
-
We'll copy over the index.ejs view and put the todos "database" into the
models
folder. -
Then we'll incorporate best-practice routing.
-
Finally, after learning about how to organize code into controllers, well, that's what we'll do.
-
Create todos/index.ejs:
$ mkdir views/todos $ touch views/todos/index.ejs
-
Add the HTML boilerplate.
-
Update the title to:
<title>Express To-Do</title>
-
Here's the EJS from yesterday to copy/paste:
<body> <h1>Todos</h1> <ul> <% todos.forEach(function(t) { %> <li> <%= t.todo %> - <%= t.done ? 'done' : 'not done' %> </li> <% }); %> </ul> </body>
-
Now let's create and copy over our model.
-
Create models/todo.js:
$ touch models/todo.js
-
Note that modules for models should be named singularly.
-
Here's the code from yesterday, just slightly refactored:
const todos = [ {todo: 'Feed Dogs', done: true}, {todo: 'Learn Express', done: false}, {todo: 'Buy Milk', done: false} ]; module.exports = { getAll }; function getAll() { return todos; }
-
Since we need a router for our todos resource and don't need the routes/users.js router module that Express Generator created, we'll modify it instead of having it lay around unused.
-
First, rename the routes/users.js route module to a name that's more appropriate for our resource - routes/todos.js.
-
The renaming of routes/users.js to routes/todos.js requires a couple of changes in server.js; both when the router module is being required:
// around line 8 var todosRouter = require('./routes/todos');
and when it's being mounted:
// around line 23 app.use('/todos', todosRouter);
-
The following is the index route code for the to-dos we used yesterday.
-
Copy it into routes/todos.js below the existing route and then we'll refactor it:
// existing route above app.get('/todos', function(req, res) { res.render('todos/index', { todos: todoDb.getAll() }); });
-
Now for the refactor...
-
We'll delete that existing route in a moment, but first note its path...
Why is it only a forward slash? -
Okay, update our route's path to
/
and delete the existing route. -
Notice how we're calling
todoDb.getAll()
- this will currently cause an error...
-
We first need to require the Todo model as follows:
var router = express.Router(); // require the Todo model var Todo = require('../models/todo');
-
It's convention to name model variables singularly and with upper-camel-casing.
-
With the model required, what do we need to change on this line of code?
todos: todoDb.getAll()
-
Let's do it!
-
There's another change that need to be made
does anybody see it? -
Hint: What's that
app
object doing there?
-
With the refactor complete, browsing to
localhost:3000/todos
should render the to-dos just like yesterday! -
Hey, let's add a link on views/index.ejs so that we can click it to see the to-dos instead of navigating via the address bar...
-
In views/index.ejs:
<!DOCTYPE html> <html> <head> <title><%= title %></title> <link rel='stylesheet' href='/stylesheets/style.css' /> </head> <body> <h1><%= title %></h1> <a href="/todos">To-Do List</a> </body> </html>
-
For styling, let's copy that
<link>
over to todos/index.ejs and...
-
In routes/index.js, fix the value of the
title
property being passed to the view:res.render('index', { title: 'Express To-Do' });
-
That's better.
-
On to controllers...
- In a web application that follows the MVC architectural pattern, controllers:
- Use Models to perform CRUD (create, retrieve, update & delete) data operations.
- Implement any additional application logic, often relying on other services and utility modules; and
- Pass data to Views to be rendered then return the resulting markup to the browser.
-
Controllers are functions, but wait, we already wrote functions that perform those responsibilities in our route modules!
-
Exactly! Those functions are controllers, we just need to separate our concerns, i.e., as a best practice, we need to separate the route definitions from their respective controller functions.
-
Let's start by creating a controller module for the todos resource:
$ touch controllers/todos.js
-
Let's copy just the function part of the following route definition:
router.get('/', function(req, res) { res.render('todos/index', { todos: Todo.getAll() }); });
-
Paste that function inside of controllers/todos.js...
-
Let's export the
index
controller method (also know as a controller action)... -
The pasted and fixed up code should look like:
module.exports = { index }; function index(req, res) { res.render('todos/index', { todos: Todo.getAll() }); }
-
The above is a good approach to follow when it comes to exporting functionality.
-
The router no longer needs the
Todo
model. -
But, the controller does! Let's go cut it from routes/todos.js and paste it at the top of controllers/todos.js:
var Todo = require('../models/todo');
-
Back in routes/todos.js, we need to require the controller in order to have access to its actions (methods):
var todosCtrl = require('../controllers/todos');
-
Now, the refactor:
router.get('/', todosCtrl.index);
How clean is that?!?!
-
Refresh and everything should be hunky-dory!
-
Notice how we now have the following for the todos resource:
- models/todo.js
- views/todos (directory)
- controllers/todos.js
- routes/todos.js
-
Each data resource should receive the same treatment.
-
Note that resource names are pluralized except for the model.
-
In our web apps, we will often need to pass information, such as an identifier for a certain data resource, in the path of the HTTP request.
-
URL Parameters, also known as Route Parameters, just like parameters in functions, provide a way for data to be passed in to the router & controller via the URL of the request.
-
Let's look at this analogy...
-
In Express, we define route parameters in the path string using a colon, followed by the parameter name.
-
Let's say we want to view a details page for a resource.
-
Just like how we use an index route/action to list all of a resource, we will use a show route/action when displaying the details of a single resource.
-
Let's add the functionality to view a single To Do...
-
When adding functionality to your apps, start by identifying what route makes sense - this is usually based on RESTful/Resourceful Routing conventions.
-
We'll definitely be reviewing RESTful/Resourceful Routing later, in fact we just might quiz you on it one day - it's that important 😊
-
According to REST, the "proper" route to display a
single To Do would be:GET /todos/:id
-
With the proper route identified, the next step is to create some UI that will send a request that matches that route...
-
Let's refactor todos/index.ejs as follows:
<% todos.forEach(function(t, idx) { %> <li> <a href="/todos/<%= idx %>"><%= t.todo %></a>
Don't forget to add the
idx
parameter in the callback function -
Refresh the page and hover over the links. Looking at the bottom-left of the window will verify the paths look correct!
-
Links always send an HTTP request using what HTTP method?
-
The UI is set to send the proper HTTP requests to the server.
-
However, clicking one of those links will display a
Not Found 404 error - this means that there is no route on the server that matches the HTTP request. -
Let's add one...
-
Add the show route below the index route as follows:
router.get('/', todosCtrl.index); router.get('/:id', todosCtrl.show);
The actual path is
/todos/:id
- right? -
Saving will crash the app because there is no
todosCtrl.show
being exported from the controller...
-
Add the
show
action inside of controllers/todos.js and don't forget to export it!function show(req, res) { res.render('todos/show', { todo: Todo.getOne(req.params.id), todoNum: parseInt(req.params.id) + 1 }); }
-
Express's
req.params
object will have a property for each route parameter defined, for example...
-
A route defined like this:
router.get('/category/:catName/page/:pageNo', ...);
and a link like this:
<a href="/category/socks/page/2">Next Page</a>
would have a
req.params
available in the controller of:console.log(req.params.catName) //=> "socks" console.log(req.params.pageNo) //=> "2"
-
Note that all route param values are strings.
-
Another refresh informs us that the
show
action in the controller is calling aTodo.getOne
method that doesn't exist. -
Let's fix that error! In models/todo.js:
module.exports = { getAll, getOne }; function getOne(id) { return todos[id]; }
-
Refresh and of course there's an error because we haven't created the views/todos/show.ejs that we're trying to render.
-
Copy the boilerplate from views/todos/index.ejs and then add this:
<body> <h1>Todo #<%= todoNum %></h1> <h3><%= todo.todo %></h3> <h3>Complete: <%= todo.done ? 'Yes' : 'No' %></h3> </body>
-
Refresh - BAM!
Use the Routing Guide as necessary...
Assume a data resource of cats
when answering the following:
-
What will the name of the router module be? (include its parent directory)
-
Write the line of code within server.js that would require the above router and assign it to a variable named
catsRouter
. -
Write the line of code within server.js that would mount the above router object prefixing the proper path.
Using the router
object within routes/cats.js
and assuming a cats controller assigned to a variable named catsCtrl
:
-
Write the line of code that defines the proper route that would read/display all cats (cats index route).
-
Write the line of code that defines the proper route that would read/display a single cat (cats show route).
Using the router
object within routes/cats.js
and assuming a cats controller assigned to a variable named catsCtrl
:
-
Write the line of code that defines the proper route that would display a view that includes a form for submitting a new cat (cats new route).
-
Write the line of code that defines the proper route that would handle the cat form being submitted and creates a new cat (cats create route).
Note: When searching for info on the Express framework, be sure that you search for the info for version 4 only - there were significant changes made from earlier versions. Also note that version 5 is currently in alpha although all of the code we've written should be compatible.