Class 22: Express.js - Middleware
In Express.js, middleware functions are the
heart of how requests are processed. They are functions that
have access to the request object (req
), the
response object (res
), and the next middleware
function in the application’s request-response cycle.
Middleware functions can perform tasks like:
- Executing any code.
- Making changes to the request and the response objects.
- Ending the request-response cycle.
- Calling the next middleware in the stack.
Understanding Middleware
Imagine your Express application as an assembly line for HTTP requests. Each station on the assembly line is a middleware function. A request enters the first station, gets processed, and then is passed to the next station, and so on, until a station decides to send a response back to the client.
The key to middleware is the next()
function. When
a middleware function completes its task, it calls
next()
to pass control to the next middleware
function in the stack. If a middleware function does not call
next()
, it must terminate the request-response
cycle itself (e.g., by sending a response with
res.send()
or res.json()
).
// server.js
const express = require('express');
const app = express();
const port = 3000;
// This is a simple custom middleware function
const myLogger = function (req, res, next) {
console.log('LOGGED:', req.method, req.url, new Date().toISOString());
next(); // Call next() to pass control to the next middleware/route handler
};
// Apply the middleware to all routes in the application
app.use(myLogger);
// Define a route
app.get('/', (req, res) => {
res.send('Hello World!');
});
app.get('/users', (req, res) => {
res.send('Users list page');
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
When you run this server and visit /
or
/users
, you'll see the log message from
myLogger
in your terminal before the "Hello World!"
or "Users list page" response is sent.
Types of Middleware
Express.js supports several types of middleware:
1. Application-level middleware:
Bound to an instance of the app
object by using
app.use()
or app.METHOD()
(e.g.,
app.get()
, app.post()
).
// Applied to all requests
app.use(myLogger);
// Applied only to GET requests to /api/products
app.get('/api/products', (req, res, next) => {
// This middleware runs only for this specific route
console.log('Accessing products API...');
next();
}, (req, res) => {
res.json({ products: [] });
});
2. Router-level middleware:
Works in the same way as application-level middleware, but it is
bound to an instance of express.Router()
. This
allows you to modularize your routes and middleware.
// routes/userRoutes.js
const express = require('express');
const router = express.Router();
// Middleware specific to this router
router.use((req, res, next) => {
console.log('Time:', Date.now());
next();
});
// Define routes for this router
router.get('/', (req, res) => {
res.send('Users homepage');
});
router.get('/:id', (req, res) => {
res.send(`User: ${req.params.id}`);
});
module.exports = router;
// In your main server.js:
// const userRoutes = require('./routes/userRoutes');
// app.use('/users', userRoutes); // Mount the router at /users
3. Error-handling middleware:
These functions have four arguments:
(err, req, res, next)
. They are specifically
designed to catch and handle errors that occur during the
request-response cycle.
// Must be the last middleware loaded
app.use((err, req, res, next) => {
console.error(err.stack); // Log the error stack for debugging
res.status(500).send('Something broke!'); // Send a generic error response
});
4. Built-in middleware:
Express comes with built-in middleware functions like
express.static
, express.json
, and
express.urlencoded
.
5. Third-party middleware:
Middleware functions loaded using npm
, such as
morgan
for logging, cors
for handling
Cross-Origin Resource Sharing, or helmet
for
setting security headers.
Built-in Express Middleware
Express provides several built-in middleware functions to handle common tasks.
-
express.json()
:Parses incoming requests with JSON payloads. This middleware populates the
req.body
property with the parsed JSON data. Essential for building APIs that receive JSON data (e.g., from frontend forms or API clients).// server.js const express = require('express'); const app = express(); const port = 3000; app.use(express.json()); // Enable JSON body parsing app.post('/api/data', (req, res) => { console.log('Received data:', req.body); // req.body will contain the parsed JSON res.json({ message: 'Data received!', yourData: req.body }); }); app.listen(port, () => { console.log(`Server running at http://localhost:${port}`); });
To test this, you would use a tool like Postman or Insomnia and send a POST request to
http://localhost:3000/api/data
with aContent-Type: application/json
header and a JSON body like{"name": "Alice", "age": 30}
. -
express.urlencoded()
:Parses incoming requests with URL-encoded payloads. This is typically used for parsing data from HTML forms submitted with
Content-Type: application/x-www-form-urlencoded
.// server.js // ... app.use(express.urlencoded({ extended: true })); // Enable URL-encoded body parsing app.post('/submit-form', (req, res) => { console.log('Form data:', req.body); // req.body will contain the parsed form data res.send(`Thanks for submitting, ${req.body.username}!`); }); // ...
-
express.static()
:Serves static files such as HTML files, CSS files, images, and JavaScript files. This is crucial for serving your frontend assets.
// server.js // ... // Serve static files from a directory named 'public' // If you have index.html in 'public', it will be served at the root URL app.use(express.static('public')); // Example: If you have public/css/style.css, it can be accessed at http://localhost:3000/css/style.css // If you have public/index.html, it can be accessed at http://localhost:3000/ // ...
For this to work, create a folder named
public
in your project root and put some files in it (e.g.,public/index.html
,public/styles.css
).
Creating Custom Middleware
You can write your own custom middleware functions to add specific logic to your application.
// server.js
const express = require('express');
const app = express();
const port = 3000;
// Custom middleware for authentication check
const authenticateUser = (req, res, next) => {
const isAuthenticated = true; // In a real app, check token, session, etc.
if (isAuthenticated) {
req.user = { id: 1, name: 'John Doe', role: 'admin' }; // Attach user info to request
next(); // User is authenticated, proceed to the next middleware/route
} else {
res.status(401).send('Unauthorized: Please log in.'); // Not authenticated
}
};
// Custom middleware for logging request details
const requestLogger = (req, res, next) => {
const start = Date.now();
res.on('finish', () => { // Event listener for when the response is finished
const duration = Date.now() - start;
console.log(`${req.method} ${req.originalUrl} - ${res.statusCode} - ${duration}ms`);
});
next();
};
app.use(requestLogger); // Apply logging middleware globally
// Apply authentication middleware only to routes under /admin
app.get('/admin', authenticateUser, (req, res) => {
res.send(`Welcome, admin ${req.user.name}!`); // Access user info from req.user
});
app.get('/', (req, res) => {
res.send('Public homepage');
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
Common Use Cases for Middleware
- Logging requests: Recording details about incoming requests (e.g., IP address, URL, timestamp, HTTP method).
- Authentication and authorization checks: Verifying user identity and permissions before allowing access to routes.
- Data parsing: Parsing JSON, URL-encoded, or multipart form data from request bodies.
- Error handling: Catching and processing errors that occur during the request cycle.
- Setting headers: Adding or modifying HTTP response headers (e.g., security headers, CORS headers).
- Input validation: Ensuring that incoming data meets specific criteria before processing.
Middleware is a fundamental and powerful concept in Express.js. By chaining middleware functions, you can build complex and modular request processing pipelines, keeping your code clean and organized. In the next class, we'll explore RESTful API principles, which will guide us in structuring our Express.js APIs.