This presentation can be viewed here
# Express Middleware
- Students Will Be Able To:
- Describe the Purpose of Middleware
- Use
method-override
Middleware and HTML Forms to Add, Update & Delete Data on the Server - Use Query Strings to Provide Additional Information to the Server
- Setup
- What is Middleware?
- Our First Middleware
- Key Middleware
- Express Request-Response Cycle
- Creating To-Dos
- method-override Middleware
- Delete a To-Do
- Exercise: Update a To-Do
-
This lesson builds upon the
express-todos
project created in the previous lesson. -
If your previous code has the
index
&show
routes working, you may continue to build upon it, however, if it wasn't working, or you just want to play it safe, there is starter code provided:- Be sure to
cd
into thestarter-code/express-todos
folder - Then install the node modules:
$ npm install
- Be sure to
-
Make sure that you're in the express-todos folder and open your text editor.
-
In the Intro to Express lesson, we identified the three fundamental capabilities provided by web application frameworks:
- The ability to define routes
- The ability to process HTTP requests using middleware
- The ability to use a view engine to render dynamic templates
-
We've already defined routes and rendered dynamic templates.
-
In this lesson we complete the trifecta by processing requests using middleware.
-
A middleware is simply a function with the following signature:
function(req, res, next) {}
-
As you can see, middleware have access to the request (
req
) and response (res
) objects - this allows middleware to modify them in anyway they see fit. -
Once a middleware has done its job, it either calls
next()
to pass control to the next middleware in the pipeline or ends the request as we've been doing with therender
&redirect
methods...
-
Yes, actually you have already written middleware - the controller actions,
todosCtrl.index
&todosCtrl.show
, are middleware! -
The controller middleware functions didn't need to define the
next
parameter because they were at the end of the middleware pipeline. That is, they ended the request/response cycle by calling a method on theres
object, e.g.,res.render
. -
The
next
function is also used for error handling.
-
There's no better way to understand middleware than to see one in action.
-
Open server.js and add this "do nothing" middleware:
app.set('view engine', 'ejs'); // add middleware below the above line of code app.use(function(req, res, next) { console.log('Hello SEI!'); next(); });
-
Type
nodemon
to start the server, browse tolocalhost:3000
, and check terminal.
-
Note that
app.use
mounts middleware functions into the middleware pipeline. -
Let's add a line of code that modifies the
req
object:app.use(function(req, res, next) { console.log('Hello SEI!'); // Add a time property to the req object req.time = new Date().toLocaleTimeString(); next(); });
-
Now let's pass this info to the todos/index.ejs view...
-
It's the responsibility of controllers to pass data to views.
-
Let's update the
index
action in controllers/todos.js so that it passesreq.time
:function index(req, res) { res.render('todos/index', { todos: Todo.getAll(), time: req.time // add this line }); }
-
Now let's render the time in todos/index.ejs by updating the
<h1>
as follows:<h1>Todos as of <%= time %></h1>
-
Refresh!
-
The order that middleware is mounted matters!
-
In server.js, let's move our custom middleware below where the routers are being mounted:
app.use('/', indexRouter); app.use('/todos', todosRouter); app.use(function(req, res, next) { console.log('Hello SEI!'); req.time = new Date().toLocaleTimeString(); next(); });
-
Refresh shows that it no longer works :(
-
Move it back above the routes - that's better.
-
Express generator mounted several key middleware by default...
-
morgan: Logger that logs requests.
-
express.json & express.urlencoded (formerly known as
body-parser
): Parses data sent in the body of the request and populates areq.body
object containing that data. -
cookie-parser: Populates a
cookies
property on thereq
object so that you can access data in cookies. For example,req.cookies.sid
. cookie-parser is middleware which deals with the incoming request. To set a cookie, you would use thecookie
object on the response object. -
express.static: Serves static assets, such as css, js and image files.
- Again, here's a great flow to follow when you want to add functionality to your web app:
- Identify the "proper" Route (Verb + Path)
- Create the UI that issues a request that matches that route.
- Define the route on the server and map it to a controller action.
- Code and export the controller action.
res.render
a view in the case of aGET
request, orres.redirect
if data was changed.
-
What functionality do we want? Do we want to show a form on the
index
view, or do we want a separate page dedicated to adding a To Do? Typically, you'd want have the form on the same page, however, for completeness, we'll use the dedicated page approach. -
Checking the Resourceful Routing for CRUD Operations in Web Applications Chart, we find that the proper route is:
GET /todos/new
-
Next step is to add a link in views/todos/index.ejs that will invoke this route:
... </ul> <a href="/todos/new">Add To-Do</a> </body>
-
Step 2 is done. On to step 3 - defining the route on the server...
-
Let's add the
new
route in routes/todos.js as follows:router.get('/', todosCtrl.index); router.get('/new', todosCtrl.new); router.get('/:id', todosCtrl.show);
-
I'm going to post this question in Slack for you to REPLY to:
Why must thenew
route be defined before theshow
route?
-
Step 4 says to code the
todosCtrl.new
action we just mapped to thenew
route: -
In controllers/todos.js:
module.exports = { index, show, new: newTodo }; function newTodo(req, res) { res.render('todos/new'); }
-
Note that you cannot name a function using a JS reserved word.
-
Now we need that
new
view. -
Create views/todos/new.ejs, copy over the boilerplate from another view, then put this good stuff in there:
<body> <h1>New Todo</h1> <form action="/todos" method="POST" autocomplete="off"> <input type="text" name="todo"> <button type="submit">Save Todo</button> </form> </body>
-
FYI that
autocomplete="off"
attribute will prevent the sometimes annoying autocomplete feature of inputs. -
Verify that clicking the Add To-Do link displays the page with the form...
-
Performing a Create data operation using a form is a two-request process.
-
Checking the Routing Chart again shows that the proper route for creating data on the server is:
POST /todos
-
That's why the form's attributes have been set to:
action="/todos"
method="POST"
-
Check this out if you want to learn more about HTML Forms.
- Same process:
- Determine proper route - check!
- Create UI - check!
- Define the route on the server - next...
-
In routes/todos.js:
router.get('/:id', todosCtrl.show); router.post('/', todosCtrl.create); // add this route
-
Yay! Our first non-
GET
route!
- Same process:
- Determine proper route - check!
- Create UI - check!
- Define the route on the server - check!
- Code and export the controller action - next...
-
In controllers/todos.js:
... create }; function create(req, res) { console.log(req.body); req.body.done = false; Todo.create(req.body); res.redirect('/todos'); }
-
Temporarily comment out the
Todo.create(req.body);
line so that we can check out what gets logged out...
-
req.body
is courtesy of this middleware in server.js:app.use(express.urlencoded({ extended: false }));
-
The properties on
req.body
will always match the values of the<input>
'sname
attributes:<input type="text" name="todo">
-
We already did Step 5 with the
res.redirect
. -
All we need is that
create
in models/todo.js:module.exports = { getAll, getOne, create }; function create(todo) { todos.push(todo); }
-
Test it out!
-
Note that when
nodemon
restarts the server, added to-dos will be lost.
-
As shown on the Resourceful Routing for CRUD Operations in Web Applications Chart, performing full-CRUD data operations requires that the browser send
DELETE
&PUT
requests. -
Using JavaScript (AJAX), the browser can send HTTP requests with any method, however, HTML can only send
GET
&POST
methods. So what do we do if we want to delete a To-Do? -
method-override middleware to the rescue!
-
Using
method-override
allows browsers to inform the server that it actually wants it to consider the request it sends to be something other than aPOST
- as you'll soon see, we'll be using forms with method="POST". -
First we need to install the middleware:
$ npm i method-override
-
Require it below
logger
in server.js:var logger = require('morgan'); var methodOverride = require('method-override');
-
Now let's add
method-override
to the middleware pipeline:app.use(express.static(path.join(__dirname, 'public'))); app.use(methodOverride('_method')); // add this
-
We are using the Query String approach for
method-override
as documented here.
-
The user story reads:
As a User, I want to delete a To Do from the list -
Same process:
- Determine proper route
-
The RESTful route is:
DELETE /todos/:id
- Same process:
- Determine proper route - check!
- Create UI - next...
-
By default,
method-override
only listens forPOST
requests. -
Therefore, we'll use a
<form>
for the UI in views/todos/index.ejs:<% todos.forEach(function(t, idx) { %> <li> <form action="/todos/<%= idx %>?_method=DELETE" class="delete-form" method="POST"> <button type="submit">X</button> </form>
-
The
?_method=DELETE
is the query string.
-
Let's some styling in public/stylesheets/style.css:
.delete-form { display: inline-block; margin-right: 10px; } .delete-form button { color: red; } li { list-style: none; }
-
Refresh and use DevTools to ensure the links look correct.
- Same process:
- Determine proper route - check!
- Create UI - check!
- Define the route on the server - next...
-
I bet you could have done this one on your own!
-
In routes/todos.js:
router.post('/', todosCtrl.create); router.delete('/:id', todosCtrl.delete);
- Same process:
- Determine proper route - check!
- Create UI - check!
- Define the route on the server - check!
- Code and export the controller action - next...
-
Similar to
newTodo
, we can't name a functiondelete
, so...create, delete: deleteTodo }; function deleteTodo(req, res) { Todo.deleteOne(req.params.id); res.redirect('/todos'); }
-
Any questions?
-
All that's left is to add the
deleteOne
method to theTodo
model:module.exports = { getAll, getOne, create, deleteOne }; function deleteOne(id) { todos.splice(id, 1); }
-
Does it work? Of course it does!
- Updating a To-Do is very similar to creating one because it also is a two-request process:
- One request to display a form used to edit the To-Do.
- Another request to submitted the form to the server so that it can update the To-Do.
- Exercise #1:
- As a User, when viewing the show page for a To-Do, I want to be able to click a link to edit the text of the To-Do
- Exercise #2:
- As a User, when editing a To-Do, I want to be able to toggle whether or not it's done
- Hints:
- Follow the same steps we followed multiple times for adding functionality!
- Be sure to reference the Routing Chart to determine the proper routes!
- You will want to pre-fill the
<input>
with the todo text - use thevalue
attribute and some EJS to pull this off. - Don't forget that the controller action will first have to get the To-Do being edited so that it can be sent to the view.
-
Hints for Exercise #2 (Toggling
done
):- Use an
<input type="checkbox" ...>
- Checkboxes are checked when a
checked
attribute exists (no value is assigned). - Use a ternary expression to write in the
checked
attribute, or an empty string. - If the checkbox is checked when submitted,
req.body.done
will have the value of"on"
, otherwise there won't even be areq.body.done
property.
- Use an
-
Enjoy!
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.