W03 Learning Activity: Middleware
Overview
In this activity you will learn more about middleware in Express and add some middleware functions to your application.
Preparation Material
As your Express applications grow more complex, you encounter recurring challenges: How do you authenticate users across multiple routes? How do you log requests consistently? How do you validate input data without repeating the same code everywhere? How do you handle errors gracefully throughout your entire application?
These challenges have a common solution in Express: middleware. You have already used middleware without realizing it every time you called app.use(express.static()) or created error handlers. Now it is time to understand middleware deeply, learning how it works and how to create your own middleware functions that make your applications more organized, maintainable, and powerful.
The Problem Middleware Solves
Imagine building an e-commerce site where certain routes require user authentication, others need admin privileges, some need request logging, and all of them need error handling. Without middleware, you would end up copying the same authentication code, logging code, and error handling code into every single route handler. Your code would become repetitive, difficult to maintain, and prone to inconsistencies.
Middleware solves this by creating a pipeline system where each request passes through a series of functions before reaching its final destination. Each function in the pipeline can perform specific tasks like checking authentication, logging requests, validating data, or handling errors. This creates a clean separation of concerns where each piece of functionality has its own dedicated function.
What Exactly is Middleware?
Middleware in Express is any function that has access to the request (req) and response (res) objects, along with the ability to pass control to the next function in the pipeline. Think of middleware as checkpoints that every request must pass through on its journey from arriving at your server to sending a response back to the user.
Each middleware function can examine the request, modify it, perform operations like database queries or external API calls, and then either send a response immediately or pass control to the next middleware function. This creates a flexible, modular system where you can compose complex application behavior from simple, focused functions.
Middleware as a Factory Assembly Line
Think of middleware like stations on an assembly line. Each station (middleware function) performs a specific task on the product (HTTP request) as it moves down the line. Some stations might add components, others might inspect quality, and the final station packages the finished product (sends the response). Each station can either continue the process or stop it if something is wrong.
Three Types of Middleware Functions
Express recognizes different types of middleware based on their function signatures, allowing it to handle each type appropriately in the request-response cycle:
Generic Middleware
Signature: (req, res, next) => { ... }
This is the most common type of middleware, used for any logic that needs access to the request and response objects but is not the final handler. Generic middleware typically performs operations like authentication, logging, data validation, or request modification, then calls next() to continue the pipeline.
Notice that generic middleware functions have three parameters, including next.
Route Handlers
Signature: (req, res) => { ... }
Route handlers are the final destination in the middleware pipeline for successful requests. They typically send the response using methods like res.send(), res.render(), or res.json(). Since they end the request-response cycle, they do not need access to the next function.
Notice that route handlers have only two parameters, they do not have a next.
Error-Handling Middleware
Signature: (err, req, res, next) => { ... }
Error-handling middleware has four parameters instead of three, with the error object as the first parameter. Express uses this signature to identify error handlers and only invokes them when an error occurs. These functions provide centralized error processing for your entire application.
Notice that error-handling middleware functions have four parameters, including err and next.
The following code snippet shows an example of each kind of middleware function.
// Generic middleware - logs all requests
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next(); // Continue to next middleware
});
// Route handler - ends the request-response cycle
app.get('/home', (req, res) => {
res.send('Welcome home!');
});
// Error handler - processes errors
app.use((err, req, res, next) => {
console.error(err.message);
res.status(500).send('Something went wrong!');
});
The Critical Role of next()
The next() function is what links middleware together, creating the pipeline that processes requests. When a middleware function calls next(), it tells Express to continue to the next middleware or route handler in the chain. If next() is never called, the request-response cycle stops early, leaving the client waiting forever (the request hangs).
The next() function has different behaviors depending on what you pass to it:
-
next()– Continue to the next non-error middleware -
next(err)– Skip all remaining non-error middleware and jump to the first error handler -
next("route")– Skip remaining middleware for the current route and try the next route
The following code snippet shows an example where the first middleware function checks if the request header contains an authorization token and either continues the request or triggers an error.
// Middleware that conditionally continues or stops
app.use((req, res, next) => {
if (!req.headers['authorization']) {
// Create error and skip to error handler
const err = new Error('Authorization header required');
err.status = 401;
return next(err);
}
// All good, continue to next middleware
next();
});
// This only runs if authorization header exists
app.use((req, res, next) => {
console.log('User is authenticated');
next();
});
// Error handler catches authentication failures
app.use((err, req, res, next) => {
res.status(err.status || 500).send(err.message);
});
Common Middleware Mistake
Forgetting to call next() is one of the most common middleware mistakes. If you do not call next() and do not send a response, the client will hang waiting for a response that never comes. Always ensure your middleware either calls next() or sends a response.
Sharing Data Between Middleware: req and res.locals
The req (request) and res (response) objects are shared across all middleware functions in the pipeline, making them perfect for passing data between middleware. While you can attach properties directly to the req object, Express provides res.locals as a more organized and conventional approach for storing data that should be available to other middleware and templates.
The following code snippet demonstrates how to use res.locals to share data between middleware functions.
// Middleware to determine user role
app.use((req, res, next) => {
// Set user role based on query parameter or default to guest
res.locals.userRole = req.query.role || 'guest';
res.locals.timestamp = new Date().toISOString();
next();
});
// Middleware that uses the role data
app.use((req, res, next) => {
console.log(`User role: ${res.locals.userRole}`);
next();
});
// Route handler that uses the data
app.get('/dashboard', (req, res) => {
res.send(`
<h1>Dashboard</h1>
<p>Role: ${res.locals.userRole}</p>
<p>Time: ${res.locals.timestamp}</p>
`);
});
Why res.locals is Special
Data stored in res.locals is automatically accessible to template engines like EJS without needing to pass it in the res.render() function call. This makes it perfect for data that multiple templates need, like user information or site configuration. However, be mindful that all templates can access this data, so avoid storing sensitive information there.
Global Middleware: Affecting Every Request
Global middleware applies to every incoming request and is typically used for application-wide functionality like logging, security headers, or setting up common data. Global middleware should be defined early in your application, before route-specific logic, so it can affect all subsequent requests.
The following code snippet demonstrates how global middleware can run and perform various actions for all requests.
// Global request logging
app.use((req, res, next) => {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] ${req.method} ${req.url}`);
next();
});
// Global security headers
app.use((req, res, next) => {
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-Content-Type-Options', 'nosniff');
next();
});
// Global user context setup
app.use((req, res, next) => {
// Set default user context
res.locals.isLoggedIn = false;
res.locals.userName = 'Guest';
// In a real app, you'd check authentication here
next();
});
// All routes defined after this point will have these middleware applied
app.get('/', (req, res) => {
res.send(`Hello, ${res.locals.userName}!`);
});
Route-Specific Middleware: Selective Application
Route-specific middleware runs only for specific routes, allowing you to apply logic selectively rather than globally. This is particularly useful for functionality like authentication checks, input validation, or specialized logging that only applies to certain endpoints.
You can apply middleware to specific routes by passing middleware functions as arguments to route methods before the final route handler. Express will execute them in order, and each must call next() to continue to the next function.
The following code snippet demonstrates how to create and apply route-specific middleware functions.
Notice how the logAccess, requireAdmin, and requireAuth middleware functions are first defined, and then they are applied to specific routes later by including them as arguments in the route definitions.
// Define reusable middleware functions
const logAccess = (req, res, next) => {
console.log(`Access attempt to ${req.url}`);
next();
};
const requireAdmin = (req, res, next) => {
// In a real app, you'd check actual authentication
if (req.headers.role !== 'admin') {
return res.status(403).send('Admin access required');
}
next();
};
const requireAuth = (req, res, next) => {
if (!req.headers.authorization) {
return res.status(401).send('Authentication required');
}
next();
};
// Public route - no middleware needed
app.get('/', (req, res) => {
res.send('Welcome to our site!');
});
// Protected route - requires authentication
app.get('/profile', requireAuth, (req, res) => {
res.send('Your profile information');
});
// Admin route - requires both logging and admin check
app.get('/admin', logAccess, requireAdmin, (req, res) => {
res.send('Admin dashboard');
});
// Multiple middleware functions in sequence
app.get('/secure-admin', logAccess, requireAuth, requireAdmin, (req, res) => {
res.send('Super secure admin area');
});
Activity Instructions
In this activity, you will add middleware functions to your application.
Add middleware to log every request
In this step, you will add a middleware function that will log every request to your application. This will help you understand how middleware processes every request that comes to your server.
In your server.js file, add the following code to come after the code that sets the view engine and the views directory, but directly before your route definitions.
// Middleware to log all incoming requests
app.use((req, res, next) => {
if (NODE_ENV === 'development') {
console.log(`${req.method} ${req.url}`);
}
next(); // Pass control to the next middleware or route
});
Save your file and navigate to different pages in your application. Watch your console to see the logs for each request. This demonstrates how middleware runs for every request before reaching your route handlers.
Add middleware to make NODE_ENV available
In this step, you will create another middleware function to make the environment variable NODE_ENV available to all your templates. This will be helpful in the future, when you create an error page and want to display detailed error information only when NODE_ENV is set to development.
Add the following code directly after your previous middleware function that logs every request.
// Middleware to make NODE_ENV available to all templates
app.use((req, res, next) => {
res.locals.NODE_ENV = NODE_ENV;
next();
});
Update your footer
Now that you have made the NODE_ENV variable available to all your templates, you can update your footer to use this variable dynamically.
Update your src/views/partials/footer.ejs file to include a conditional message that displays "(Development mode)" after the copyright, only when NODE_ENV is set to development.
Update your footer.ejs file to look as follows:
<footer>
<p>© <%= new Date().getFullYear() %> CSE 340 Service Network.
<% if (NODE_ENV === 'development') { %>
(Development mode)
<% } %>
</p>
</footer>
</body>
</html>
Save your file and refresh any page in your application. Your footer should now show the current year and indicate if you are in development mode.
Next Step
Complete the other Week 03 Learning Activities
After you have completed all the learning activities for this lesson, return to Canvas to submit a quiz.
Other Links:
- Return to: Week Overview | Course Home