Skip to main content

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

  1. Setup
  2. What is Middleware?
  3. Our First Middleware
  4. Creating To-Dos
  5. method-override Middleware
  6. Delete a To-Do
  7. Further Study

Videos

  1. Video 📹 Link
  2. Video 📹 Link
  3. Video 📹 Link
  4. Video 📹 Link

1. Setup

This lesson continues to build upon the express-todos project.

  1. Move into the repo/project folder:

    cd ~/code/express-todos
  2. Open project in VS Code:

    code .
  3. Open an Integrated Terminal (control + backtick) and start the Express server:

    nodemon
  4. 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:

  1. The ability to define routes
  2. The ability to process HTTP requests using middleware
  3. 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:

server.js
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:

server.js
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();
});
tip

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 with res.locals.

Now we can add the time property in by updating the <h1> as follows:

controllers/todos.js
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:

todos/index.ejs
<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:

server.js
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.

caution

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:

  1. Browser sends an initial request to see a page that includes a form to input the data, and...

  2. 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 resource
  • edit + 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:

  1. Identify the "proper" Route (HTTP Method + Path)

  2. Create the UI (<a> or <form>) that will send an HTTP request that matches that route.

  3. Define the route on the server and map it to a controller action.

  4. Code and export the controller action.

  5. res.render() a view in the case of a GET request, or res.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:

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:

views/todos/index.ejs
...
</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:

routes/todos.js
// 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?


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:

controllers/todos.js
module.exports = {
index,
show,
new: newTodo,
};

function newTodo(req, res) {
res.render("todos/new", { title: "New Todo" });
}
caution

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:

views/todos/new.ejs
<%- 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 set action="/todos" in a bit.
note

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)

  1. Use the Routing Chart to identify the proper route (HTTP Method & Endpoint) for creating a To-Do data resource on the server.

  2. Comment the proper route as we've been doing within routes/todos.js

SKIP creating the UI - use Insomnia to issue the request

Step 2 - Create the UI that issues the request

The <form> we already completed is the UI and it's ready for business!

Check this out if you want to learn more about HTML Forms.

✅ Step 1 - Determine proper route

✅ Step 2 - Create UI

Step 3 - Define the Route

In routes/todos.js:

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:

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:

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:

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

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?


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:

server.js
var logger = require("morgan");
var methodOverride = require("method-override");

Now let's add method-override to the middleware pipeline:

server.js
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:

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:

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.

Step 3 - Define the route on the server

I bet you could have done this one on your own!

In routes/todos.js:

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

controllers/todos.js
  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:

models/todo.js
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);
}
tip

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 ActionPurpose
indexDisplay 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
showDisplay a single data resource
deleteDelete 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.