Students Will Be Able To: |
---|
Use the Express Generator to Scaffold a Skeleton App |
Implement Best Practice Routing |
Organize App Logic Into Controller Modules |
Implement Show (Read One) Functionality |
- Setup
- Express Generator
- MVC Code Organization
- Best Practice Routing
- To-Do Refactor
- Controller Modules
- Routing Quiz
- MVC Organization Revisited
- URL/Route Parameters
- Adding "Show a To-Do" Functionality
To get ready for this lesson, fork and clone this 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.
The first thing we'll take a look at in this lesson 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.
- Has key middleware configured and mounted by default.
- Is configured to serve static assets from a
public
folder. - Will configure the EJS view engine (if we specify the
-e
option) - Has error handling configured.
We install express-generator
globally using NPM:
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.
If your system won't install or run express-generator, it can be used with npx
instead as described in a bit below.
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'll definitely want to use the EJS view engine each time we scaffold a new Express project.
The help above identifies the following option flags to make EJS the view engine:
-e
-ejs
--view=ejs
-v=ejs
All of the above option flags do the exact same thing, kind of like the shortest one 😀
Let's learn more about Express by building a simple To-Do app.
We scaffold an app using the express
command:
express -e express-todos
Note that when a name is specified,
express-generator
creates a folder with that name and creates the app within it. If you are already in an existing folder, you can runexpress -e .
to scaffold the app within that folder.
If express-generator
did not install or if the above express
command did not work, use npx
instead as follows:
npx express -e express-todos
We now need to cd
into the new folder and open in our text editor:
cd express-todos
code .
Express generator has created a package.json
that lists the necessary Node modules, however, those modules are not installed.
Let's open a terminal session (option
+ backtick
) and install them:
npm i
Let's briefly review the scaffolded contents in VS Code:
├── app.js
├── bin
│ └── www
├── package.json
├── public
│ ├── images
│ ├── javascripts
│ └── stylesheets
│ └── style.css
├── routes
│ ├── index.js
│ └── users.js
└── views
├── error.js
└── index.js
MERN/MEAN Stack apps often have a client-side file named app.js
and this could get confusing having two app.js
files, thus many developers name their main Express file server.js
.
So let's rename it:
-
Rename
app.js
toserver.js
. -
Inside of
bin/www
, we need to update line 7 to requireserver
instead ofapp
:var app = require('../app');
to:
var app = require('../server');
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 type nodemon
which will use that same start
script.
Browsing to localhost:3000
greets us with:
MVC (Model-View-Controller) has been a proven approach for organizing code for decades where the:
-
Model: Represents the data concern and encapsulates the business logic of the application. It manages and manipulates the data.
-
View: Represents the presentation concern and is responsible for displaying data to users and capturing user input.
-
Controller: Acts as the processing concern in the MVC architecture. It receives HTTP requests from the browser, processes them by performing CRUD operations on the data using models, and ultimately responds to the browser's request. This response can involve rendering views, returning resulting HTML, or issuing a redirect. The controller controls the flow of data and the overall behavior of the application, serving as an intermediary between the Model and the View.
Express, as it states on its landing page, is unopinionated. This means we are free to structure and organize our Express apps anyway we please.
In fact, many web frameworks such as Ruby on Rails, ASP.net, Spring MVC (Java), and others implement the MVC architectural pattern.
Accordingly, most Express developers use MVC to organize their Express applications as well...
Express generator has already organized the view templates into a views
folder.
So we just need to make folders to hold our model and controller modules:
mkdir models controllers
With the models
, views
and controllers
folders set up, we're ready to discuss best practice routing...
In our first-express
app, we used the app.get
method to define routes and although it worked, the better practice is to:
- Use Express
router
objects to define routes for a particular purpose or dedicated to a certain data resource such astodos
. - Create each
router
in its own module from which it is exported. - Inside of server.js
require
and mount therouter
object in the request pipeline.
Note: A data resource is a "type" of data/information that applications create, read, update and/or delete (CRUD).
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 data resource, in this case, users.
Note how routes are defined on those two router
objects using router.get()
method call just like we did previously with app.get()
Each router
object has one route defined - compare those two routes, notice the HTTP methods and the paths? They're the same - isn't that a problem? Nope, they're not actually the same because of the way the routers are mounted in server.js...
The two route modules are required on lines 7 & 8 of server.js
.
Then those routers are mounted in the middleware pipeline with the app.use
method on lines 22 & 23:
app.use('/', indexRouter);
app.use('/users', usersRouter);
IMPORTANT KEY POINT: The path specified in
app.use
is a "starts with path". It is prepended to the paths specified in the router object forming the actual path.
Let's say you have a router
object that defines a route like this:
// routes/todos.js
var express = require('express');
var router = express.Router();
router.get('/', function(req, res) {...
and is mounted like this:
const todosRouter = require('./routes/todos');
// All routes defined in todosRouter will start with /todos
app.use('/todos', todosRouter);
❓ What is the actual path of the route?
The starts with path is /todos
and the path of the defined route is just /
which doesn't change the actual path, thus the actual path is /todos
Another example, let's say you have a router
object that defines a route like this:
// routes/calendar.js
var express = require('express');
var router = express.Router();
router.get('/today', function(req, res) {...
and is mounted like this:
const calendarRouter = require('./routes/calendar');
app.use('/calendar', calendarRouter);
❓ What is the actual path of the above route?
The starts with path is /calendar
and the path of the defined route is /today
making the actual path /calendar/today
We're going to refactor the To-Do code from yesterday to follow best practices by:
-
Copying over the index.ejs view and put the todos "database" into the
models
folder. -
Implementing best-practice routing.
-
Organizing the route handling code into a controller functions (AKA controller actions).
-
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 (replacing the existing
<body>
element):<!DOCTYPE html> <html> <head> <title><%= title %></title> <link rel='stylesheet' href='/stylesheets/style.css' /> </head> <body> <h1>Express To-Do</h1> <ul> <% todos.forEach( todo => { %> <li> <%= todo.todo %> - <%= todo.done ? 'done' : 'not done' %> </li> <% }); %> </ul> </body> <html>
-
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 slightly refactored code from yesterday, copy it into models/todo.js:
const todos = [ {id: 125223, todo: 'Feed Dogs', done: true}, {id: 127904, todo: 'Learn Express', done: false}, {id: 139608, todo: 'Buy Milk', done: false} ]; const getAll = () => { return todos; } module.exports = { getAll };
Look it over - any questions?
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:
// server.js // around line 8 var todosRouter = require('./routes/todos');
and when it's being mounted:
// around line 23 app.use('/todos', todosRouter);
-
In routes/todos.js, let's remove the following code:
// DELETE the following... /* GET users listing. */ router.get('/', function(req, res, next) { res.send('respond with a resource'); });
and add the following comment to remind us of how the router was mounted:
// All actual paths start with "/todos"
-
We now want to define the route for the To-Dos index functionality (display all To-Dos). However, we are not going to write an anonymous inline function for the route handler. Instead, we are going to follow a best practice of putting the function in a controller module that can export any number of controller actions (functions).
-
Here's the route that uses a controller action that we'll code in a bit:
// routes/todos.js var express = require('express'); var router = express.Router(); // Require the controller that exports To-Do CRUD functions var todosCtrl = require('../controllers/todos'); // All actual paths begin with "/todos" // GET /todos router.get('/', todosCtrl.index);
Is that route definition tidy or what?!?!
-
Note that the Express app is unhappy because the controller we required and the
index
function do not yet exist. That's next...
In a web application that follows the MVC architectural pattern, controllers:
- Handle the request coming from the client (browser).
- Implement the application logic such as using Models to perform CRUD data operations (create, retrieve, update & delete), fetching data from an API, etc.
- Respond to the request by rendering views (often passing data to them) or issuing a redirect.
- Sespond with json in the case of a React app.
Controller functions are no different than the inline functions that we've already seen!
We just want to separate our concerns, i.e., we want 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
Yes, only modules for models are named singularly.
-
Here's the function we used yesterday, just refactored into a function declaration named
index
. Let's copy/paste it into controllers/todos.js:// controllers/todos.js const index = (req, res) => { const todos = Todo.getAll(); res.render('todos/index', { todos }); };
-
Let's export the
index
controller method (also know as a controller action):// controllers/todos.js const index = (req, res) => { const todos = Todo.getAll(); res.render('todos/index', { todos }); }; module.exports = { index };
A common approach is to export an object near the top because you don't have to scroll to the bottom of the module to see what functionality is being exported. However, in order to do this your functions must be written in the function declaration style due to the way hoisting works with arrow functions. So instead, we're going to export our controller module's functionality beneath our functions.
-
Yup, the controller module is going to need to require that Todo model:
// controllers/todos.js // Convention is to name the model in uppercase and singular const Todo = require('../models/todo'); const index = (req, res) => { const todos = Todo.getAll(); res.render('todos/index', { todos }); }; module.exports = { index };
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>
-
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!
👀 Refer to the RESTful Routes to CRUD Mapping Guide as necessary
(1) What will the name of the router module be? (include its parent directory)
(2) Write the line of code within server.js that would require the above router and assign it to a variable named catsRouter
.
(3) Write the line of code within server.js that would mount the above router object prefixing the proper path of /cats
.
For the following questions, assume a cats controller assigned to a variable named catsCtrl
within routes/cats.js
- write the following code within routes/cats.js
:
(4) Write the line of code that defines the proper route that would read/display all cats (cats index route).
(5) Write the line of code that defines the proper route that would read/display a single cat (cats show route).
(6) 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).
(7) Write the line of code that defines the proper route that would handle the cat form being submitted and creates a new cat in the database (cats create route).
Notice how we now have the following for the todos data resource:
- models/todo.js
- views/todos (directory)
- controllers/todos.js
- routes/todos.js
Again, everything is named plurally except the model.
Each data resource should receive the same treatment. For example, if you had a cats data resource, another set of the above modules would be created and dedicated to CRUDing cats.
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 the RESTful Routes to CRUD Mapping you referred to while taking the quiz.
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( todo => { %>
<li>
<a href="/todos/<%= todo.id %>"><%= todo.todo %></a>
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?
GET
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:
// GET /todos
router.get('/', todosCtrl.index);
// GET /todos/:id
router.get('/:id', todosCtrl.show);
Saving will crash the app because there is no todosCtrl.show
method being exported from the controller...
Add the show
action inside of controllers/todos.js and don't forget to export it!
const show = (req, res) => {
const todo = Todo.getOne(req.params.id);
res.render('todos/show', {
todo
});
};
KEY POINT: Express's
req.params
object will have a property for each route parameter defined, for example...
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 a Todo.getOne
method that doesn't exist.
Let's fix that error! In models/todo.js:
getOne = (id) => {
return todos.find(todo => todo.id === parseInt(id));
}
module.exports = {
getAll,
getOne
};
Refresh and of course there's an error because we haven't created the views/todos/show.ejs that we're trying to render.
Touch the views/todos/show.ejs.
Copy the markup from views/todos/index.ejs and then add this:
<body>
<h3><%= todo.todo %></h3>
<h3>Complete: <%= todo.done ? 'Yes' : 'No' %></h3>
</body>
Refresh - 👏
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 beta although all of the code we've written should be compatible.