Express Middleware
Learning Objectives
-
Students Will Be Able To:
- Describe the Purpose of Middleware
- Use
method-overrideMiddleware and HTML Forms to Add, Update & Delete Data on the Server - Use Query Strings to Provide Additional Information to the Server
Roadmap
- 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
Setup
- This lesson builds upon the
express-todosproject created in the previous lesson. Make sure that you're in this folder and have it open in your text editor.
What is Middleware?
-
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&redirectmethods... - 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
nextparameter because they were at the end of the middleware pipeline. That is, they ended the request/response cycle by calling a method on theresobject, e.g.,res.render. - The
nextfunction 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 Intrepid Learner!');
next();
});- Type
nodemonto start the server, browse tolocalhost:3000, and check terminal.
Our First Middleware
- Note that
app.usemounts middleware functions into the middleware pipeline. - Let's add a line of code that modifies the
reqobject:
app.use(function(req, res, next) {
console.log('Hello Intrepid Learner!');
// 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
indexaction 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 Intrepid Learner!');
req.time = new Date().toLocaleTimeString();
next();
});- Refresh shows that it no longer works :(
- Move it back above the routes - that's better.
Key Middleware
- Here are some other pieces of middleware we'll need as we scale our application
- morgan: An
HTTPlogger that "logs" requests in the terminal. - express.urlencoded (formerly known as
body-parser): Parses data sent in the body of the request and populates areq.bodyobject containing that data. - express.static: Serves static assets, such as css, js and image files.
- The only one we need to install is
morganbecause everything else is included when we installexpress.
npm i morgan Now we require morgan and mount it as middleware
const morgan = require('morgan');We'll mount Morgan like this
app.use(morgan('dev'));Next, we'll mount our static asset middleware and bodyparser middleware
app.use(express.static('public'));
app.use(express.urlencoded({ extended: false }));...by the end, is what our "Middleware Stack" should look like
// Mount middleware (app.use)
app.use(morgan('dev'));
app.use(express.static('public'));
app.use(express.urlencoded({ extended: false }));
app.use('/', indexRouter);
app.use('/todos', todosRouter);For our static assets, we'll create a public folder at the root of our project and the place a css, js, and images subdirectory inside of it (we can also create a style.css and script.js file inside the appropriate sub-directories as well).
This is where our stylesheets, images and front-end javascript will live!
While we're at it, let's make sure all of our templates have a link tag so they can use any CSS rules we've defined in style.css
Add the following link tag to all of your template files ...(files that end with the .ejs extension inside of the views directory)
<link rel="stylesheet" href="/css/style.css">Express Request-Response Cycle
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.rendera view in the case of aGETrequest, orres.redirectif 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
newroute in routes/todos.js as follows:
router.get('/', todosCtrl.index);
router.get('/new', todosCtrl.new);
router.get('/:id', todosCtrl.show);- Why must the
newroute be defined before theshowroute? - Step 4 says to code the
todosCtrl.newaction we just mapped to thenewroute: - In controllers/todos.js:
module.exports = {
index,
show,
new: newTodo
};
function newTodo(req, res) {
res.render('todos/new');
}
// original code below...- Note that you cannot create a function using a JS reserved word like
new. - Now we need that
newview. - 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="text">
<button type="submit">Save Todo</button>
</form>
</body>Creating To-Dos
- 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.
- If you remember the routing chart from our last lesson, we can see the proper (RESTful) route is...
POST /todosThat'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-
GETroute!
Creating To-Dos (Cont)
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.bodyis courtesy of this middleware in server.js:
app.use(express.urlencoded({ extended: false }));- The properties on
req.bodywill always match the values of the<input>'snameattributes:
<input type="text" name="text">- We already did Step 5 with the
res.redirect. - All we need is that
createin models/todo.js:
module.exports = {
getAll,
getOne,
create
};
const todos = [
{text: 'Feed Dogs', done: true},
{text: 'Learn Express', done: false},
{text: 'Buy Milk', done: false}
];
function create(todo) {
todos.push(todo);
}
// Original code below- Test it out!
- Note that when
nodemonrestarts the server, added to-dos will be lost.
method-override Middleware
- Again, referring back to our routing chart from our routing lesson, performing full-CRUD data operations requires that the browser send
DELETE&PUTrequests. - Using JavaScript (AJAX), the browser can send HTTP requests with any method, however, HTML can only send
GET&POSTmethods. So what do we do if we want to delete a To-Do? - method-override middleware to the rescue!
- Using
method-overrideallows 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
morganin server.js:
const morgan = require('morgan');
const methodOverride = require('method-override');- Now let's add
method-overrideto the middleware pipeline:
app.use(express.static('public'));
app.use(methodOverride('_method')); // add this- We are using the Query String approach for
method-overrideas documented here.
Delete a To-Do
- 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-overrideonly listens forPOSTrequests. - 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=DELETEis the query string. - Let's some styling in public/css/style.css:
.delete-form {
display: inline-block;
margin-right: 10px;
}
.delete-form button {
color: red;
}
li {
list-style: none;
margin-bottom: 10px;
}- 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');
}- All that's left is to add the
deleteOnemethod to theTodomodel:
module.exports = {
getAll,
getOne,
create,
deleteOne
};
const todos = [
{text: 'Feed Dogs', done: true},
{text: 'Learn Express', done: false},
{text: 'Buy Milk', done: false}
];
function deleteOne(id) {
todos.splice(id, 1);
}
// Original code belowDoes it work? Of course it does!
💪 Exercises: Update a To-Do
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.
Update a 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 thevalueattribute 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
checkedattribute exists (no value is assigned). - Use a ternary expression to write in the
checkedattribute, or an empty string. - If the checkbox is checked when submitted,
req.body.donewill have the value of"on", otherwise there won't even be areq.body.doneproperty.
- Use an
- Enjoy!
References
Official Documentation | Express.js
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.