Topic 006: Different types of middleware implementation in Node.jS.
What is Middleware in Node Js?
- Middleware in Node.js refers to a concept where functions can be used to process incoming requests before they reach their final destination and handle outgoing responses before they are sent back to the client. These functions sit in between the initial request and the final response, hence the term “middleware.”
How Does Node.js Middleware Pattern Work?
-
In Node.js, middleware functions are essentially functions that have access to the request object (req), the response object (res), and the next function in the application's request-response cycle.
-
When a request is made to the server, it passes through a series of middleware functions before reaching the final route handler or endpoint.
-
Each middleware function can perform its task and either pass the request to the next middleware function using the next function or terminate the request-response cycle by sending a response.
What happens when the request reaches the last middleware in the chain ?
-
The request passes through all middleware functions in the chain.
-
If none of the middleware functions send a response back to the client (i.e., they all call the next() function to pass control to the next middleware or route handler):
-
Express looks for a matching route handler based on the request URL and HTTP method.
-
If a matching route handler is found, it is executed
-
If no matching route handler is found, Express sends a default “Not Found” response back to the client with a status code of 404.
What is next() ?
-
In Express.js, next() is a function that is used within middleware functions to pass control to the next middleware function in the chain. When next() is called within a middleware function, Express moves to the next middleware function defined in the application.
-
When a middleware function is defined, it typically receives three arguments: req (the request object), res (the response object), and next (the next middleware function in the chain). By calling next() within a middleware function, the control is passed to the subsequent middleware function.
Middleware Chaining
-
Generally, a set of middlewares are chained to form a set of functions that execute one after the other in order.
-
The next() function is called at the end of every middleware to pass the control to the next middleware. The last middleware function sends back the response to the client. Hence, different middleware process the request before the response is sent back.
-
Purpose: To log details about each incoming request, including the method, URL, and response time.
-
Example:
const logger = (req, res, next) => { console.log(`${req.method} ${req.url}`); const start = Date.now(); res.on("finish", () => { const duration = Date.now() - start; console.log(`${req.method} ${req.url} took ${duration}ms`); }); next(); }; app.use(logger);
-
Purpose: To protect routes by ensuring that only authenticated users can access certain endpoints.
-
JWT Authentication: Middleware to verify JSON Web Tokens in requests to secure endpoints.
-
OAuth Middleware: Implemented middleware to handle OAuth flows, such as with Google or Facebook login.
-
Example:
const authenticate = (req, res, next) => { const token = req.header("Authorization"); if (!token) { return res.status(401).send("Access denied. No token provided."); } try { const decoded = jwt.verify(token, process.env.JWT_SECRET); req.user = decoded; next(); } catch (ex) { res.status(400).send("Invalid token."); } }; app.use("/api/protected", authenticate);
-
Here’s a simplified example of JWT authentication middleware in Node.js using the
jsonwebtokenlibrary:
const jwt = require("jsonwebtoken");
function authenticateToken(req, res, next) {
const token = req.header("Authorization") && req.header("Authorization").split(" ")[1];
if (!token) return res.sendStatus(401); // Unauthorized
jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, user) => {
if (err) return res.sendStatus(403); // Forbidden
req.user = user;
next();
});
}
module.exports = authenticateToken;-
Purpose: To handle errors centrally and return a consistent error response format.
-
Example:
const errorHandler = (err, req, res, next) => { console.error(err.message, err); res.status(500).send("Something failed."); }; app.use(errorHandler);
-
Purpose: To parse incoming request bodies so they can be easily accessed in route handlers.
-
Example:
const express = require("express"); const bodyParser = require("body-parser"); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true }));
-
Purpose: To enable Cross-Origin Resource Sharing (CORS) for allowing resources to be requested from another domain.
-
Example:
const cors = require("cors"); const corsOptions = { origin: "http://example.com", optionsSuccessStatus: 200, }; app.use(cors(corsOptions));
-
Purpose: To limit the number of requests from a single IP address to prevent abuse or DDoS attacks.
-
Example:
const rateLimit = require("express-rate-limit"); const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // limit each IP to 100 requests per windowMs }); app.use(limiter);
-
Purpose: To serve static files such as images, CSS files, and JavaScript files.
-
Example:
const express = require("express"); app.use(express.static("public"));
-
Purpose: To validate incoming request data to ensure it meets the expected format and constraints.
-
Example:
const { check, validationResult } = require("express-validator"); const validateUser = [ check("email").isEmail().withMessage("Must be a valid email"), check("password").isLength({ min: 5 }).withMessage("Password must be at least 5 chars long"), (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } next(); }, ]; app.post("/register", validateUser, (req, res) => { // Handle registration });
- Middleware like
compressionto gzip responses, reducing the payload size and improving performance.
-Certainly! Compression middleware is used to reduce the size of the response body, which can help speed up web applications by reducing the amount of data that needs to be transferred over the network. In Node.js, this can be achieved using the compression middleware.
Here is an example of how to implement compression middleware in a Node.js application using Express:
-
Install the
compressionpackage: You need to install thecompressionpackage from npm. You can do this using the following command:npm install compression
-
Set up the middleware in your Express application: Once the package is installed, you can use it in your Express application as shown below:
const express = require("express"); const compression = require("compression"); const app = express(); // Use compression middleware app.use(compression()); // Example route to test compression app.get("/", (req, res) => { res.send("Hello, this is a compressed response!"); }); const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`Server is running on port ${PORT}`); });
-
Importing the necessary modules:
const express = require("express"); const compression = require("compression");
-
Creating an instance of the Express application:
const app = express();
-
Using the compression middleware:
app.use(compression());
This line adds the compression middleware to your application, which will compress all HTTP responses by default.
-
Defining a route to test compression:
app.get("/", (req, res) => { res.send("Hello, this is a compressed response!"); });
You can visit this route in your browser or use a tool like
curlto see the compressed response. -
Starting the server:
const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`Server is running on port ${PORT}`); });
This code starts the server on the specified port.
You can configure the compression middleware by passing options. For example, you might want to set a threshold for compressing responses only if their size exceeds a certain number of bytes.
app.use(
compression({
threshold: 1024, // Compress responses only if their size is > 1KB
})
);- This middleware can greatly enhance the performance of your web application, especially if your application serves large amounts of data or if it is accessed frequently by users with slow internet connections.
Query string parsers are middleware functions in Node.js applications, particularly in frameworks like Express, used to parse and validate query parameters attached to the URL. These parameters are typically appended to the end of a URL after a question mark (?) and are key-value pairs separated by ampersands (&).
The query string parser middleware parses these parameters from the URL and makes them available to your application's route handlers. Additionally, you can implement validation logic within the middleware to ensure that the query parameters meet certain criteria or constraints.
Here's an example of how to implement a query string parser middleware with validation using Express:
const express = require("express");
const app = express();
// Query string parser middleware
const parseQuery = (req, res, next) => {
// Assuming we expect a 'name' parameter in the query string
const name = req.query.name;
// Validation logic
if (!name) {
return res.status(400).send("Name parameter is required.");
}
// Optionally, you can further validate the parameter's format or values
// Attach the parsed query parameter to the request object for easy access in route handlers
req.parsedQuery = { name };
// Call next to pass control to the next middleware or route handler
next();
};
// Apply the query string parser middleware to all routes
app.use(parseQuery);
// Example route that uses the parsed query parameter
app.get("/greet", (req, res) => {
const { name } = req.parsedQuery;
res.send(`Hello, ${name}!`);
});
// Start the server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});In this example:
- We define a middleware function called
parseQuerythat extracts thenameparameter from the query string. If thenameparameter is missing, the middleware sends a 400 Bad Request response. - The parsed query parameter (in this case, just
name) is attached to thereqobject for easy access in subsequent route handlers. - We apply the
parseQuerymiddleware to all routes usingapp.use(parseQuery). - In the
/greetroute handler, we access the parsed query parameter (name) fromreq.parsedQueryand use it to send a personalized greeting response.
This is a basic example, but you can extend it to handle more complex query parameters or implement additional validation logic as needed for your application.
Certainly! Here's how you can implement each of the mentioned middleware topics with code:
const checkUserRole = (req, res, next) => {
// Assuming user role is stored in req.user.role
if (req.user.role !== "admin") {
return res.status(403).send("Access denied. You are not authorized to access this resource.");
}
next();
};
// Applying middleware to a specific route
app.get("/admin/dashboard", checkUserRole, (req, res) => {
res.send("Welcome to the admin dashboard.");
});const bodyParser = require("body-parser");
// Parsing JSON bodies
app.use(bodyParser.json());
// Parsing URL-encoded bodies
app.use(bodyParser.urlencoded({ extended: true }));
// Parsing multipart/form-data
const multer = require("multer");
const upload = multer();
app.use(upload.none());const parseQuery = (req, res, next) => {
const name = req.query.name;
if (!name) {
return res.status(400).send("Name parameter is required.");
}
req.parsedQuery = { name };
next();
};
app.use(parseQuery);const morgan = require("morgan");
app.use(morgan("combined"));const customLogger = (req, res, next) => {
console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`);
next();
};
app.use(customLogger);const { body, validationResult } = require("express-validator");
const validateRequest = [
body("email").isEmail(),
body("password").isLength({ min: 6 }),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
next();
},
];
app.post("/register", validateRequest, (req, res) => {
// Register user
});const helmet = require("helmet");
app.use(helmet());const cors = require("cors");
const corsOptions = {
origin: "http://example.com",
optionsSuccessStatus: 200,
};
app.use(cors(corsOptions));const csrf = require("csurf");
const csrfProtection = csrf({ cookie: true });
app.get("/payment", csrfProtection, (req, res) => {
// Render payment form with CSRF token
});const session = require("express-session");
const MongoStore = require("connect-mongo")(session);
app.use(
session({
secret: "secret",
resave: false,
saveUninitialized: true,
store: new MongoStore({ url: "mongodb://localhost/sessions" }),
})
);- Parse cookie header and populate req.cookies.
var express = require("express");
var cookieParser = require("cookie-parser");
var app = express();
app.use(cookieParser());
app.get("/", function (req, res) {
// Cookies that have not been signed
console.log("Cookies: ", req.cookies);
// Cookies that have been signed
console.log("Signed Cookies: ", req.signedCookies);
});
app.listen(8080);
// curl command that sends an HTTP request with two cookies
// curl http://127.0.0.1:8080 --cookie "Cho=Kim;Greet=Hello"This code provides examples of each middleware type with their implementation in a Node.js/Express application. You can adapt and customize them according to your specific requirements and business logic.
These are some of the middleware implementations that I have worked on in my projects, each serving a specific purpose to enhance the functionality, security, and maintainability of the applications.