Skip to content

Instantly share code, notes, and snippets.

@jim-clark
Last active October 17, 2020 06:08
Show Gist options
  • Save jim-clark/07f9b2f08932561aa03cd2c17d38783b to your computer and use it in GitHub Desktop.
Save jim-clark/07f9b2f08932561aa03cd2c17d38783b to your computer and use it in GitHub Desktop.

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>


Express
Routers & Controllers


Learning Objectives


  • Students Will Be Able To:
    • Use the Express Generator to Scaffold a Skeleton App
    • Implement Best Practice Routing
    • Organize App Logic Into Controllers

Roadmap


  • 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

Setup


  • To get ready for this lesson, simply cd into today's folder for the class repo.

express-generator


express-generator


  • 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.


express-generator


  • 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.

express-generator


  • 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.


express-generator

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

Generating Our App's Skeleton with
express-generator


  • 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

Folder Structure

  • 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...


Starting the Application


  • 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.


Starting the Application

  • Browsing to localhost:3000 greets us with:


Renaming app.js


  • MERN/MEAN Stack apps often have a client-side file named app.js and this could get confusing having two app.js files. This is why many developers name their main Express file server.js.

  • So let's rename it...


Renaming app.js


  • First, rename app.js to server.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.


MVC Code Organization


MVC Code Organization


  • 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.


MVC Code Organization


  • 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

Best Practice Routing


Best Practice Routing


  • In our first-express app, we used the app.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 as todos.
    • Create each router in its own module from which it is exported.
    • require the exported router inside of server.js.
    • Mount the router object in the request pipeline (route objects are also middleware functions).

Best Practice Routing


  • 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.


Best Practice Routing


  • 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.

The Express Router Object


  • Note how routes are defined on those two router objects using a get method just like we did with app:
    router.get() instead of app.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 Express Router Object


  • 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...


The Express Router Object


  • 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?


The Express Router Object


  • 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?


To-Do Refactor


  • 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.


To-Do Refactor - index.ejs


  • 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>


To-Do Refactor - index.ejs


  • 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>

To-Do Refactor - Todo Model


  • 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.


To-Do Refactor - Todo Model


  • 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;
     }

To-Do Refactor - Routing


  • 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.


To-Do Refactor - Routing


  • 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);

To-Do Refactor - Routing


  • 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...


To-Do Refactor - Routing


  • 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...


To-Do Refactor - Routing


  • 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.


To-Do Refactor - Routing


  • With the model required, what do we need to change on this line of code?

     todos: todoDb.getAll()
  • Let's do it!


To-Do Refactor - Routing


  • There's another change that need to be made
    does anybody see it?

  • Hint: What's that app object doing there?


To-Do Refactor


  • 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...


To-Do Refactor


  • 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...


To-Do Refactor


  • 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...


Controllers



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


  • 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.


Controllers


  • 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...


Controllers

  • 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.


Controllers


  • 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');

Controllers


  • 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!


MVC Organization Revisited


MVC Organization Revisited


  • 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.


URL/Route Parameters


URL/Route Parameters


  • 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...


URL/Route Parameters



URL/Route Parameters


  • 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...


Adding Show To-Do Functionality


Adding Show a To-Do Functionality


  • 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 😊


Adding Show a To-Do Functionality


  • 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...


Adding Show a To-Do Functionality


  • 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?


Adding Show a To-Do Functionality


  • 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...


Adding Show a To-Do Functionality


  • 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...


Adding Show a To-Do Functionality


  • 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...


Adding Show a To-Do Functionality


  • 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.


Adding Show a To-Do Functionality


  • 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:

     module.exports = {
       getAll,
       getOne
     };
     
     function getOne(id) {
       return todos[id];
     }

Adding Show a To-Do Functionality


  • 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!


Routing Quiz



Use the Routing Guide as necessary...


Routing Quiz


Assume a data resource of cats when answering the following:

  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.


Routing Quiz


Using the router object within routes/cats.js and assuming a cats controller assigned to a variable named catsCtrl:

  1. Write the line of code that defines the proper route that would read/display all cats (cats index route).

  2. Write the line of code that defines the proper route that would read/display a single cat (cats show route).


Routing Quiz


Using the router object within routes/cats.js and assuming a cats controller assigned to a variable named catsCtrl:

  1. 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).

  2. 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).


Congrats!

You Did It!


References


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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment