Lesson 36-Express Middleware Mechanism

Middleware Principles and Usage

Middleware Concept

In Express, middleware is a function that processes HTTP requests and responses. It executes before the request reaches the route handler and can perform tasks such as modifying request and response objects, terminating the response process, or passing control to the next middleware function in the stack.

Middleware Structure

Middleware functions come in the following forms:

  • Standard Middleware: function(req, res, next)
  • Error-Handling Middleware: function(err, req, res, next)

Here, req is the request object, res is the response object, and next is a function that passes control to the next middleware or route handler.

Middleware Registration

Middleware is registered in an Express application using app.use() for global middleware or .use() for route-specific middleware.

const express = require('express');
const app = express();

// Global middleware
app.use((req, res, next) => {
    console.log('A request has been made!');
    next();
});

// Route-specific middleware
app.use('/api', (req, res, next) => {
    console.log('Request made to /api');
    next();
});

app.get('/api/data', (req, res) => {
    res.send('API data');
});

app.listen(3000);

Middleware Execution Order

Middleware executes in the order it is registered. Each middleware can decide whether to pass control to the next middleware by calling next().

Error-Handling Middleware

Error-handling middleware takes four parameters: err, req, res, and next. It is invoked when an error is thrown anywhere in the application.

app.use((err, req, res, next) => {
    console.error(err.stack);
    res.status(500).send('Something broke!');
});

Example Application

Let’s analyze the middleware mechanism with a detailed code example:

const express = require('express');
const app = express();
const port = 3000;

// Logging middleware
app.use((req, res, next) => {
    console.log(`Request received at ${new Date().toISOString()}`);
    next();
});

// Authentication middleware
app.use((req, res, next) => {
    if (req.headers.authorization === 'Bearer token') {
        next();
    } else {
        res.status(401).send('Unauthorized');
    }
});

// Route-specific middleware
app.use('/users', (req, res, next) => {
    console.log('Handling a request to /users');
    next();
});

// User data route
app.get('/users/:id', (req, res) => {
    const userId = req.params.id;
    res.json({ id: userId, name: 'John Doe' });
});

// Error-handling middleware
app.use((err, req, res, next) => {
    console.error(err.stack);
    res.status(500).send('Something broke!');
});

app.listen(port, () => {
    console.log(`Server running at http://localhost:${port}`);
});

Analysis

  • Logging Middleware: Logs a timestamp for every incoming request.
  • Authentication Middleware: Checks the Authorization header for a Bearer token. If valid, it proceeds; otherwise, it returns a 401 status with an error message.
  • Route Middleware: Executes for requests starting with /users, then passes control to the next middleware or route handler.
  • User Data Route: Handles GET requests to /users/:id, extracts the user ID from the request parameters, and returns a JSON response.
  • Error-Handling Middleware: Catches any unhandled errors in the application and returns a 500 status with an error message.

Advanced Middleware Usage

a. Conditional Middleware

Middleware can execute selectively based on conditions, such as only for POST requests:

app.use(['POST'], (req, res, next) => {
    if (req.method === 'POST') {
        console.log('Handling a POST request');
        next();
    } else {
        next();
    }
});

However, a more recommended approach is to use route-specific middleware:

app.post('/endpoint', (req, res) => {
    // ...
});

b. File Upload Middleware

File uploads are common in web applications. Middleware like multer simplifies handling file uploads:

const multer = require('multer');
const upload = multer({ dest: 'uploads/' });

app.post('/upload', upload.single('image'), (req, res) => {
    console.log(req.file);
    res.send('File uploaded successfully');
});

c. Authentication Middleware

Authentication middleware verifies user credentials, typically before route handlers. For example, using JWT for authentication:

const jwt = require('jsonwebtoken');

app.use((req, res, next) => {
    const token = req.headers['authorization'];
    if (!token) return res.status(401).send('Access denied');
    try {
        const decoded = jwt.verify(token, 'secret');
        req.user = decoded;
        next();
    } catch (ex) {
        res.status(400).send('Invalid token');
    }
});

Middleware Composition and Reuse

Middleware can be composed and reused to avoid code duplication. For example, combining logging and authentication middleware:

const logger = (req, res, next) => {
    console.log(`Request received at ${new Date().toISOString()}`);
    next();
};

const authenticate = (req, res, next) => {
    // Authentication logic...
    next();
};

const authLogger = [logger, authenticate];

app.use(authLogger);

Middleware Debugging and Testing

Debug middleware using console.log to trace request flow. For testing, use libraries like supertest to simulate HTTP requests and verify middleware behavior.

const request = require('supertest');
const app = require('./app');

describe('Middleware tests', () => {
    it('should log requests', done => {
        request(app)
            .get('/')
            .expect(200, done);
    });
});

Writing Custom Middleware

Understanding Middleware Structure

Middleware functions typically accept four parameters: req (request object), res (response object), next (callback function), and an optional err (error object). A standard middleware function looks like this:

function middlewareFunction(req, res, next) {
    // Perform operations
    next(); // Pass control to the next middleware or route handler
}

Writing Custom Middleware

Let’s start with a simple logging middleware:

function loggerMiddleware(req, res, next) {
    console.log(`${req.method} request received for ${req.url} at ${new Date().toISOString()}`);
    next();
}

module.exports = loggerMiddleware;

Using Custom Middleware

Import and register the custom middleware in the Express application using app.use():

const express = require('express');
const app = express();
const loggerMiddleware = require('./loggerMiddleware');

app.use(loggerMiddleware);

app.get('/', (req, res) => {
    res.send('Hello World!');
});

app.listen(3000, () => {
    console.log('Server is running on port 3000');
});

Passing Parameters to Middleware

Middleware can accept parameters for use within the function:

function paramMiddleware(param) {
    return function(req, res, next) {
        console.log(`Parameter passed: ${param}`);
        next();
    };
}

app.use(paramMiddleware('customParam'));

Route-Level Middleware

Middleware can be used globally or for specific routes:

app.get('/users', (req, res, next) => {
    // Route-specific middleware logic for /users
    next();
}, (req, res) => {
    res.send('List of users');
});

Error-Handling Middleware

Error-handling middleware takes four parameters (err, req, res, next) to catch and process errors during request handling:

function errorHandler(err, req, res, next) {
    console.error(err.stack);
    res.status(500).send('Something broke!');
}

app.use(errorHandler);

Complex Middleware Example: Authentication

Authentication middleware often checks for tokens in request headers to verify user login status:

const jwt = require('jsonwebtoken');

function authenticationMiddleware(req, res, next) {
    const token = req.headers['x-access-token'];
    if (!token) return res.status(401).send('No token provided');

    jwt.verify(token, 'your_jwt_secret', (err, decoded) => {
        if (err) return res.status(500).send('Failed to authenticate token');

        // If valid, store the decoded token
        req.user = decoded;
        next();
    });
}

app.use(authenticationMiddleware);

Middleware Composition and Reuse

Middleware can be composed to avoid redundancy. For example, combining logging and authentication middleware:

const combinedMiddleware = [loggerMiddleware, authenticationMiddleware];
app.use(combinedMiddleware);

Testing Middleware

Use libraries like supertest to test middleware behavior:

const request = require('supertest');
const app = require('./app');

describe('Middleware tests', () => {
    it('should log requests', done => {
        request(app)
            .get('/')
            .expect(200, done);
    });
});

Request Preprocessing with Middleware

Middleware is ideal for request preprocessing, such as parsing request bodies, validating data formats, or extracting query parameters. For example, using body-parser to parse JSON and URL-encoded data:

const bodyParser = require('body-parser');

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

Security is critical for web applications. Middleware can enhance security by preventing CSRF attacks, setting secure HTTP headers, or limiting request rates. For example, using helmet to set secure HTTP headers:

const helmet = require('helmet');

app.use(helmet());

Caching Middleware

Caching can significantly improve performance. Middleware can implement response caching to reduce database load. For example, using cache-control to manage caching strategies:

const cacheControl = require('cache-control');

app.use(cacheControl({ maxAge: 60 * 60 * 24, units: 'seconds' }));

Advanced Logging Middleware

Beyond basic logging, middleware can handle complex logging tasks, such as recording detailed request information, error stacks, or performance metrics. For example, using morgan for detailed request logging:

const morgan = require('morgan');

app.use(morgan('combined'));

Modularizing and Encapsulating Middleware

To improve code readability and maintainability, encapsulate middleware into separate modules, each handling a single function for easy management and reuse. For example, create a dedicated error-handling middleware module:

// errorMiddleware.js
module.exports = function errorMiddleware(err, req, res, next) {
    console.error(err.stack);
    res.status(500).json({ error: 'Internal Server Error' });
};

Then import and use it in the application:

const errorMiddleware = require('./errorMiddleware');

app.use(errorMiddleware);

Testing and Debugging Middleware

Testing middleware is crucial for application stability and security. Use testing frameworks like Mocha and Chai with supertest to verify middleware behavior. For example, testing whether the logging middleware correctly logs requests:

const chai = require('chai');
const chaiHttp = require('chai-http');
const app = require('./app');
const should = chai.should();

chai.use(chaiHttp);

describe('Logger Middleware', () => {
    it('should log requests', done => {
        chai.request(app)
            .get('/')
            .end((err, res) => {
                // Check log output to ensure the request was logged correctly
                done();
            });
    });
});

Performance Optimization

While powerful, excessive or improper middleware use can impact performance. When designing middleware, minimize unnecessary computations and I/O operations. For example, use compression middleware to compress responses and reduce network transfer size:

const compression = require('compression');

app.use(compression());

Third-Party Middleware

body-parser: Parsing Request Bodies

The body-parser middleware parses incoming request bodies, supporting formats like JSON and URL-encoded data.

Installation and Usage

npm install body-parser

Code Example

const express = require('express');
const bodyParser = require('body-parser');
const app = express();

app.use(bodyParser.json()); // Support JSON format
app.use(bodyParser.urlencoded({ extended: true })); // Support URL-encoded format

app.post('/data', (req, res) => {
    console.log(req.body); // Access parsed request body
    res.send('Data received');
});

app.listen(3000, () => {
    console.log('Server started on port 3000');
});

morgan: Request Logging

The morgan middleware logs HTTP requests, supporting various output formats like common and combined.

Installation and Usage

npm install morgan

Code Example

const express = require('express');
const morgan = require('morgan');
const app = express();

app.use(morgan('combined')); // Use combined format

app.get('/', (req, res) => {
    res.send('Hello, World!');
});

app.listen(3000, () => {
    console.log('Server started on port 3000');
});

helmet: Secure HTTP Headers

The helmet middleware sets secure HTTP headers to enhance web application security.

Installation and Usage

npm install helmet

Code Example

const express = require('express');
const helmet = require('helmet');
const app = express();

app.use(helmet());

app.get('/', (req, res) => {
    res.send('Secure server');
});

app.listen(3000, () => {
    console.log('Server started on port 3000');
});

The cookie-parser middleware parses cookies sent by the client.

Installation and Usage

npm install cookie-parser

Code Example

const express = require('express');
const cookieParser = require('cookie-parser');
const app = express();

app.use(cookieParser());

app.get('/', (req, res) => {
    console.log(req.cookies); // Access cookies
    res.send('Cookies received');
});

app.listen(3000, () => {
    console.log('Server started on port 3000');
});

session: Session Management

The express-session middleware manages user sessions.

Installation and Usage

npm install express-session

Code Example

const express = require('express');
const session = require('express-session');
const app = express();

app.use(session({
    secret: 'my-secret-key',
    resave: false,
    saveUninitialized: true
}));

app.get('/', (req, res) => {
    req.session.views = (req.session.views || 0) + 1; // Track visit count
    res.send(`You have visited this page ${req.session.views} times`);
});

app.listen(3000, () => {
    console.log('Server started on port 3000');
});

passport: Authentication

The passport middleware provides a flexible authentication solution, supporting various strategies.

Installation and Usage

npm install passport passport-local

Code Example

const express = require('express');
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const app = express();

passport.use(new LocalStrategy(
    function(username, password, done) {
        // Implement user lookup and password verification logic
        User.findOne({ username: username }, function(err, user) {
            if (err) { return done(err); }
            if (!user) { return done(null, false); }
            if (!user.verifyPassword(password)) { return done(null, false); }
            return done(null, user);
        });
    }
));

app.use(passport.initialize());
app.use(passport.session());

app.post('/login', passport.authenticate('local', { failureRedirect: '/login' }), (req, res) => {
    res.send('Logged in');
});

app.listen(3000, () => {
    console.log('Server started on port 3000');
});

CORS: Cross-Origin Resource Sharing

The cors middleware enables servers to accept requests from different origins, crucial for modern web applications with separated frontend and backend architectures.

Installation and Usage

npm install cors

Code Example

const express = require('express');
const cors = require('cors');
const app = express();

app.use(cors());

app.get('/api/data', (req, res) => {
    res.json({ message: 'Welcome to the API' });
});

app.listen(3000, () => {
    console.log('Server started on port 3000');
});

compression: Response Compression

The compression middleware automatically compresses response bodies, reducing transfer time and bandwidth usage for improved user experience.

Installation and Usage

npm install compression

Code Example

const express = require('express');
const compression = require('compression');
const app = express();

app.use(compression());

app.get('/', (req, res) => {
    res.send('<html><body><h1>Hello, World!</h1></body></html>');
});

app.listen(3000, () => {
    console.log('Server started on port 3000');
});

rate-limiter-flexible: Rate Limiting

The rate-limiter-flexible middleware restricts request frequency from the same IP, preventing malicious attacks and resource abuse.

Installation and Usage

npm install rate-limiter-flexible

Code Example

const express = require('express');
const RateLimiterFlexible = require('rate-limiter-flexible');
const AppRateLimiter = RateLimiterFlexible.Memory;
const app = express();

const limiter = new AppRateLimiter({
    points: 15, // Allowed requests per minute
    duration: 1, // Time window in minutes
});

app.use(async (req, res, next) => {
    try {
        await limiter.consume(req.ip);
        next();
    } catch (err) {
        res.status(429).send('Too Many Requests');
    }
});

app.get('/', (req, res) => {
    res.send('Welcome to the API');
});

app.listen(3000, () => {
    console.log('Server started on port 3000');
});

helmet: HTTP Header Security

The helmet middleware provides multiple HTTP header settings to enhance web application security, such as preventing XSS attacks and clickjacking.

const express = require('express');
const helmet = require('helmet');
const app = express();

app.use(helmet.contentSecurityPolicy({
    directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'", "'unsafe-inline'"],
        styleSrc: ["'self'", "'unsafe-inline'"],
        imgSrc: ["'self'", "data:"],
        connectSrc: ["'self'"],
        fontSrc: ["'self'"],
        frameAncestors: ["'none'"],
        objectSrc: ["'none'"],
        mediaSrc: ["'self'"],
        childSrc: ["'self'"],
        formAction: ["'self'"],
    },
}));

app.get('/', (req, res) => {
    res.send('Secure server');
});

app.listen(3000, () => {
    console.log('Server started on port 3000');
});

winston: Logging

The winston library is a flexible logging solution supporting multiple transport methods, such as files, consoles, and remote systems.

Installation and Usage

npm install winston

Code Example

const express = require('express');
const winston = require('winston');
const app = express();

const logger = winston.createLogger({
    level: 'info',
    format: winston.format.json(),
    transports: [
        new winston.transports.File({ filename: 'error.log', level: 'error' }),
        new winston.transports.File({ filename: 'combined.log' })
    ]
});

app.use((req, res, next) => {
    logger.info(`Request received: ${req.method} ${req.path}`);
    next();
});

app.get('/', (req, res) => {
    res.send('Welcome to the API');
});

app.listen(3000, () => {
    console.log('Server started on port 3000');
});
Share your love