Express Middleware

Learning Objectives
Students Will Be Able To: |
---|
Describe the Use Case of Middleware |
Use urlencoded Middleware and HTML Forms to Create Data on the Server |
Use method-override Middleware and HTML Forms to Delete Data on the Server |
Use Query Strings to Provide Additional Information to the Server |
Road Map
- Setup
- What is Middleware?
- Our First Middleware
- Creating To-Dos
method-override
Middleware- Delete a To-Do
- Further Study
Videos
1. Setup
This lesson continues to build upon the express-todos
project.
-
Move into the repo/project folder:
cd ~/code/express-todos
-
Open project in VS Code:
code .
-
Open an Integrated Terminal (
control + backtick
) and start the Express server:nodemon
-
If the app is not running or if you just want to make sure you are starting with the same code as your instructor, sync your project using the following command:
git reset --hard origin/sync-2-middleware-starter
2. 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.
Middleware are Functions
A middleware is simply a function with the following signature:
function(req, res, next) {
// "Process" the request, etc.
// End by calling next()
next()
}
You're already familiar with Express's request object (req
) and response object (res
) objects.
Because they are just objects, middleware may modify them in anyway they see fit.
The next
parameter is a function provided by Express used to pass control to the next middleware in the pipeline, or alternatively, "raise" an error (a lesson for another day).
Mounting Middleware
Express' app.use() method is used to mount middleware into its middleware pipeline.
It's called a pipeline because the HTTP request flows through it.
Purpose of Middleware
Middleware can be used to perform functionality such authentication and processing the request in multitude of ways.
Let's review the purpose of each middleware mounted in our Express generated app:
Controller Functions (Route Handlers) Are Middleware
Yes, you have already written middleware - the controller actions, todosCtrl.index
& todosCtrl.show
, are technically 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 the res
object, e.g., res.render()
.
3. Our First Middleware
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(); // Pass the request to the next middleware
});
Type nodemon
to start the server, browse to localhost:3000
, and check terminal.
Let's add a line of code that modifies the req
object by adding the current time to Express's request object that then can be accessed by any subsequent middleware:
app.use(function (req, res, next) {
console.log("Hello SEI!");
// Add a time property to the res.locals object
// The time property will then be accessible when rendering a view
res.locals.time = new Date().toLocaleTimeString();
next();
});
The res.locals object can be used to provide data to a view rendered during that request. In fact, the object provided as the second arg to
res.render()
is merged withres.locals
.
Now we can add the time
property in by updating the <h1>
as follows:
function index(req, res) {
res.render("todos/index", {
todos: Todo.getAll(),
time: res.locals.time,
});
}
SKIP changing the VIEW
Now we can render the time
property in todos/index.ejs by updating the <h1>
as follows:
<h3>To-Dos as of <%= time %></h3>
Refresh!
The Order That Middleware is Mounted Matters
We call it the middleware pipeline for a reason - the request flows through the middleware in the order they are mounted using app.use
.
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!");
res.locals.time = new Date().toLocaleTimeString();
next();
});
Refresh shows that it no longer works because the router middleware are ending the request/response cycle before our "first middleware" is reached.
Move it back above the routes - yep, order of middleware matters.
👀 Do you need to sync your code?
git reset --hard origin/sync-3-first-middleware
4. Creating To-Dos
Time to add some additional functionality to our app - adding To-Dos!
What exact 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, for adding To-Dos, you'd want have the form on the same page, however, today we'll demo the dedicated page approach so that we can see how creating data is often a two-request task:
-
Browser sends an initial request to see a page that includes a form to input the data, and...
-
The second request will happen when the form is submitted to the server so that it may create the new data, in this case a To-Do, and respond to the client with a "redirect" (status code 302), i.e., tell the browser to make a new GET request.
So yes, creating and updating data in a multi-page web application is often a two-request process:
new
+create
to create a resourceedit
+update
to update a resource
Suggested Workflow to Add Functionality to a Web App
In SEI, we don't just teach the syntax of programming - we teach helpful processes and techniques, e.g., the Guide on How to Build a Browser Game, that are difficult to discover on your own.
So, you want to add a new feature to your web app?
Below summarizes the goals described by the Guide to Add a Feature to a Web App:
-
Identify the "proper" Route (HTTP Method + Path)
-
Create the UI (
<a>
or<form>
) that will send an HTTP 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. Write the view template if it does not already exist.
Okay, since we are going for the two-request approach, let's get started implementing the new
functionality that shows a page with a form to enter a new To-Do...
SKIP creating new
functionality
Step 1 - Identify the "proper" Route
Checking the Resourceful Routing for CRUD Operations in Web Applications Chart, we find that the proper route is:
GET /todos/new
Let's document this proper route by adding a comment within routes/todos.js:
// GET /todos
router.get("/", todosCtrl.index);
// GET /todos/:id
router.get("/:id", todosCtrl.show);
// GET /todos/new <-- this new route cannot not stay here!
Step 2 - Create the UI that issues the request
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>
Step 3 - Define the route on the server
Add the new
route in routes/todos.js as follows:
// GET /todos
router.get("/", todosCtrl.index);
// GET /todos/new
router.get("/new", todosCtrl.new);
// GET /todos/:id
router.get("/:id", todosCtrl.show);
❓ Why does the new
route need to be defined before the show
route?
new
route need to be defined before the show
route?Express attempts to match routes to the request in the order that they are defined.
If new
was defined after the show
route, then the text of "new" in the URL of the request, e.g.,
https://localhost:3000/todos/new
would match the :id
route parameter defined in the "show" route causing the todosCtrl.show
function to run instead of the intended todosCtrl.new
function.
Step 4 - Code and export the controller action
We need to code the todosCtrl.new
action we just mapped to the new
route...
In controllers/todos.js:
module.exports = {
index,
show,
new: newTodo,
};
function newTodo(req, res) {
res.render("todos/new", { title: "New Todo" });
}
Note that you cannot name a function using a JS reserved word, however, there's no problem with object properties.
Step 5 - Render the view & write it if necessary
We already called res.render()
, we just need to write the new.ejs template.
Create views/todos/new.ejs, then copy this good stuff:
<%- include('../partials/header') %>
<form action="/todos" method="POST" autocomplete="off">
<input type="text" name="todo">
<button type="submit">Save Todo</button>
</form>
<%- include('../partials/footer') %>
Just a basic HTML form is being used to send data to the server when the form is submitted.
When the form is submitted, its method
and action
attributes determine what method and path the HTTP request will have:
- The
method
attribute holds the HTTP method/verb. It will usually be set to "POST", but may be a "GET" when performing searches. - The
action
attribute holds the path. We'll see why we setaction="/todos"
in a bit.
Note: The
autocomplete="off"
attribute will prevent the often annoying autocomplete feature of inputs.
Verify that clicking the Add To-Do link displays the page with the form - bravo!
👀 Do you need to sync your code?
git reset --hard origin/sync-4-new-functionality
Implementing the second-request (create
) functionality
Again, creating data can take two separate requests - it depends upon the design of the app.
Let's get started implementing that second-request responsible for creating the new To-Do on the backend...
Step 1 - Identify the "proper" Route
👉 You Do - Identify the Proper Route (1 min)
-
Use the Routing Chart to identify the proper route (HTTP Method & Endpoint) for creating a To-Do data resource on the server.
-
Comment the proper route as we've been doing within routes/todos.js
SKIP creating the UI - use Insomnia to issue the request
Step 3 - Define the Route
In routes/todos.js:
router.get("/:id", todosCtrl.show);
// POST /todos
router.post("/", todosCtrl.create); // add this route
Yay - our first non-GET
route!
Step 4 - Code and export the controller action
In controllers/todos.js:
...
create
};
function create(req, res) {
console.log(req.body);
// The model is responsible for creating data
// Todo.create(req.body);
// Do a redirect anytime data is changed
// res.redirect('/todos');
}
Check out what properties on the req.body
object get logged out.
req.body
is courtesy of this middleware in server.js:
app.use(express.json());
// app.use(express.urlencoded({ extended: false }));
The properties on req.body
will always match the values of the <input>
's name
attributes:
<input type="text" name="todo" />
Okay, let's uncomment Todo.create(req.body);
and go code it!
We need is that create
in models/todo.js:
module.exports = {
getAll,
getOne,
create,
};
function create(todo) {
// Add the id
todo.id = Date.now() % 1000000;
// New todos wouldn't be done :)
todo.done = false;
todos.push(todo);
}
Note that whenever
nodemon
restarts the server, additional To-Dos will be lost because we are not using a database to save them - yet!
Congrats on creating To-Dos
👀 Do you need to sync your code?
git reset --hard origin/sync-5-create-functionality
SKIP using method-override middleware
5. method-override
Middleware
❓ Once again - Using HTML's <a>
and <form>
elements, the browser can only send HTTP requests that have _____ or _____ HTTP methods?
<a>
and <form>
elements, the browser can only send HTTP requests that have _____ or _____ HTTP methods?GET
or POST
However, the Resourceful Routing for CRUD Operations in Web Applications Chart shows that performing full-CRUD data operations requires DELETE
& PUT
requests also.
If we were using JavaScript (AJAX), we wouldn't have a problem, but we're not - so what do we do if we want to delete or update a To-Do?
method-override middleware to the rescue!
The method-override
middleware allows the request to be sent as a POST
from the browser, but be changed on the server to a DELETE
, PUT
, etc.
Because method-override
is not built into Express, we need to install it in Terminal:
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.
6. Delete a To-Do
With method-override
ready to go, let's add the functionality to delete To-Dos!
The user story reads:
As a User, I want to delete a To Do from the list
Here we go again following the same process explained in the Guide to Add a Feature to a Web App...
Step 1. Determine the proper route
We already identified the proper route when we coded the <form>
.
Cool, on to step 2...
SKIP creating the UI - use Insomnia to issue the request
Step 2 - Create the UI
By default, method-override
only listens for POST
requests, therefore we use a <form>
to send requests that need to be treated as PUT
or DELETE
on the server.
Therefore, we'll use a <form>
for the UI in views/todos/index.ejs:
<% todos.forEach(function(t) { %>
<li>
<form action="/todos/<%= t.id %>?_method=DELETE"
class="delete-form" method="POST">
<button type="submit">X</button>
</form>
The ?_method=DELETE
is the query string that method-override
looks for. If it finds it, it changes the HTTP method to whatever is specified - always us all caps, e.g., DELETE
.
How about a little styling in public/stylesheets/style.css to make the delete form look better:
.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.
Step 3 - Define the route on the server
I bet you could have done this one on your own!
In routes/todos.js:
router.post("/", todosCtrl.create);
// new route below
router.delete("/:id", todosCtrl.delete);
Step 4 - Code and export the controller action - next
Similar to newTodo
, we can't name a function delete
, so...
create,
delete: deleteTodo
};
function deleteTodo(req, res) {
Todo.deleteOne(req.params.id);
// res.redirect('/todos');
res.status(204).json(null); // send empty response back for DELETE
}
Again, according to the MVC architectural pattern, it's the Todo model's responsibility to perform the delete.
Add the deleteOne
method to the Todo
model
All that's left is to add the deleteOne
method to the Todo
model:
module.exports = {
getAll,
getOne,
create,
deleteOne,
};
function deleteOne(id) {
// All properties attached to req.params are strings!
id = parseInt(id);
// Find the index based on the id of the todo object
const idx = todos.findIndex((todo) => todo.id === id);
todos.splice(idx, 1);
}
Using the Array.prototype.filter method is another option for removing the todo.
Does it work? Of course it does!
In the next lesson, we'll see how to update a To-Do!
👀 Do you need to sync your code?
git reset --hard origin/sync-6-delete-functionality
7. Further Study
The following summarizes the name of purpose of the CRUD actions...
CRUD Action | Purpose |
---|---|
index | Display all of a data resource |
new + create | Display a form to add a new data resource + Handle the form being submitted and create the data resource |
show | Display a single data resource |
delete | Delete a single data resource |
edit + update | Display a form to edit an existing data resource + Handle the form being submitted and update the data resource |
References
When searching for info on the Express framework, be sure that the results are for version 4 - there were significant changes made from earlier versions.