bugl
bugl
HomeLearnPatternsPathsSearch
HomeLearnPatternsPathsSearch

Loading lesson path

Learn/Node.js/Building Applications
Node.js•Building Applications

Node.js Middleware

Flash cards

Review the key moves

1/4
Core idea

What is the main idea behind Node.js Middleware?

Lesson checks

Practice each idea before moving on

Short Mimo-style checks built from this lesson's code, terms, and sequence.

1Quick choice

Which statement best captures the main point of this lesson?

2Fill blank

Complete the missing token from the example code.

___.use((req, res, next) => {
3Order

Put the learning moves in the order that makes the concept easiest to apply.

Comprehensive Guide to Middleware Types
How Middleware Works in the Request-Response Cycle
Introduction to Middleware

Introduction to Middleware

Middleware is a key part of Node.js web applications, particularly in Express.js.

It provides a way to add and reuse common functionality across your application's routes and endpoints.

Key Characteristics of Middleware

  • Executes during the request-response cycle
  • Can modify request and response objects
  • Can end the request-response cycle
  • Can call the next middleware in the stack
  • Can be application-level, router-level, or route-specific

It acts as a bridge between the raw request and the final intended route handler.

At its core, middleware is a function that has access to:

  • The request object (req)
  • The response object (res)
  • The next middleware function in the application's request-response cycle

Middleware functions can perform a variety of tasks:

  • Execute any code
  • Modify request and response objects
  • End the request-response cycle
  • Call the next middleware function in the stack

Think of middleware as a series of processing layers that requests pass through before receiving a response-like an assembly line for HTTP requests.

How Middleware Works in the Request-Response Cycle

Middleware functions are executed in the order they are defined, creating a pipeline through which requests flow.

Each middleware function can perform operations on the request and response objects and decide whether to pass control to the next middleware or end the request-response cycle.

Lifecycle of a Request Through Middleware

  • Request received by the server
  • Passed through each middleware in sequence
  • Route handler processes the request
  • Response flows back through middleware (in reverse order)
  • Response sent to client

The basic pattern of middleware in Express.js follows this structure:

app.use((req, res, next) => {
 // Middleware code goes here
 console.log('Time:', Date.now());
 // Call next() to pass control to the next middleware function
 next();
});

When you call next() , the next middleware in the stack is executed.

If you don't call next() , the request-response cycle ends and no further middleware runs.

Example: A Simple Middleware Chain

const express = require('express');
const app = express();
// First middleware
app.use((req, res, next) => {
 console.log('Middleware 1: This always runs');
 next();
});
// Second middleware
app.use((req, res, next) => {
 console.log('Middleware 2: This also always runs');
 next();
});
// Route handler
app.get('/', (req, res) => {
 res.send('Hello World!');
});
app.listen(8080, () => {
 console.log('Server running on port 8080');
});

When a request is made to the root path ('/'), the following happens:

  • Middleware 1 logs a message and calls next()
  • Middleware 2 logs a message and calls next()
  • The route handler responds with "Hello World!"

Comprehensive Guide to Middleware Types

Understanding the different types of middleware helps in organizing your application's logic effectively.

Middleware can be categorized based on its scope, purpose, and how it's mounted in the application.

Choosing the Right Type: The type of middleware you use depends on your specific needs, such as whether the middleware should run for all requests or specific routes, and whether it needs access to the router instance.

In Node.js applications, especially with Express.js, there are several types of middleware:

Application-level Middleware

Application-level middleware is bound to the Express application instance using app.use() or app.METHOD() functions.

Use Cases: Logging, authentication, request parsing, and other operations that should run for every request.

Best Practices: Define application-level middleware before defining routes to ensure they run in the correct order.

Bound to the application instance using app.use() or app.METHOD() :

const express = require('express');
const app = express();
// Application-level middleware
app.use((req, res, next) => {
 console.log('Time:', Date.now());
 next();
});

Router-level Middleware

Router-level middleware works similarly to application-level middleware but is bound to an instance of express.Router() .

Use Cases: Grouping route-specific middleware, API versioning, and organizing routes into logical groups.

Advantages: Better code organization, modular routing, and the ability to apply middleware to specific route groups.

Bound to an instance of express.Router() :

const express = require('express');
const router = express.Router();
// Router-level middleware
router.use((req, res, next) => {
 console.log('Router specific middleware');
 next();
});
router.get('/user/:id', (req, res) => {
 res.send('User profile');
});
// Add the router to the app
app.use('/api', router);

Error-handling Middleware

Error-handling middleware is defined with four arguments (err, req, res, next) and is used to handle errors that occur during request processing.

Key Points

  • Must have exactly four parameters
  • Should be defined after other app.use() and route calls
  • Can be used to centralize error handling logic
  • Can forward errors to the next error handler using next(err)

Defined with four arguments instead of three (err, req, res, next):

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

Built-in Middleware

Express includes several built-in middleware functions that handle common web application tasks.

Common Built-in Middleware

  • express.json() : Parse JSON request bodies
  • express.urlencoded() : Parse URL-encoded request bodies
  • express.static() : Serve static files
  • express.Router() : Create modular route handlers

Best Practice: Always use the built-in middleware when possible as they are well-tested and maintained by the Express team.

Express comes with some built-in middleware functions:

// Parse JSON bodies
app.use(express.json());
// Parse URL-encoded bodies
app.use(express.urlencoded({ extended: true }));
// Serve static files
app.use(express.static('public'));

Third-party Middleware

The Node.js ecosystem offers numerous third-party middleware packages that extend Express functionality.

Popular Third-party Middleware

  • Helmet: Secure your app by setting various HTTP headers
  • Morgan: HTTP request logger
  • CORS: Enable CORS with various options
  • Compression: Compress HTTP responses
  • Cookie-parser: Parse Cookie header and populate req.cookies

Installation Example: npm install helmet morgan cors compression cookie-parser

External middleware that adds functionality to Express apps:

const morgan = require('morgan');
const helmet = require('helmet');
// HTTP request logger
app.use(morgan('dev'));
// Security headers
app.use(helmet());

Common Third-party Middleware: morgan (logging) helmet (security) cors (cross-origin resource sharing) compression (response compression) cookie-parser (cookie handling)

  • morgan (logging)
  • helmet (security)
  • cors (cross-origin resource sharing)
  • compression (response compression)
  • cookie-parser (cookie handling)

Creating and Using Custom Middleware

Creating custom middleware allows you to implement application-specific functionality in a reusable way.

Well-designed middleware should be focused, testable, and follow the single responsibility principle.

Best Practices for Custom Middleware

  • Keep middleware focused on a single responsibility
  • Document the middleware's purpose and requirements
  • Handle errors appropriately
  • Consider performance implications
  • Make middleware configurable through options

Creating your own middleware functions is straightforward and allows you to add custom functionality to your application.

Example: Simple Logger Middleware

// Create a simple logging middleware
function requestLogger(req, res, next) {
 const timestamp = new Date().toISOString();
 console.log(`${timestamp} - ${req.method} ${req.url}`);
 next(); // Don't forget to call next()
}
// Use the middleware
app.use(requestLogger);

Example: Authentication Middleware

// Authentication middleware
function authenticate(req, res, next) {
 const authHeader = req.headers.authorization;
 if (!authHeader) {
 return res.status(401).send('Authentication required');
 }
 const token = authHeader.split(' ')[1];
 // Verify the token (simplified)
 if (token === 'secret-token') {
 // Authentication successful
 req.user = { id: 123, username: 'john' };
 next();
 } else {
 res.status(403).send('Invalid token');
}
}
// Apply to specific routes
app.get('/api/protected', authenticate, (req, res) => {
 res.json({ message: 'Protected data', user: req.user });
});

Example: Request Validation Middleware

// Validate a user creation request
function validateUserCreation(req, res, next) {
 const { username, email, password } = req.body;
 // Simple validation
 if (!username || username.length < 3) {
 return res.status(400).json({ error: 'Username must be at least 3 characters' });
 }
 if (!email || !email.includes('@')) {
 return res.status(400).json({ error: 'Valid email is required' });
 }
 if (!password || password.length < 6) {
 return res.status(400).json({ error: 'Password must be at least 6 characters' });
 }
 // Validation passed
 next();
}
// Apply to user creation route
app.post('/api/users', validateUserCreation, (req, res) => {
 // Process valid user creation
 res.status(201).json({ message: 'User created successfully' });
});

Error-Handling Middleware

Error-handling middleware is special because it takes four parameters instead of three: (err, req, res, next).

Example: Basic Error Handler

const express = require('express');
const app = express();
// Regular route that might throw an error
app.get('/error-demo', (req, res, next) => {
 try {
 // Simulate an error
 throw new Error('Something went wrong!');
 } catch (error) {
 next(error); // Pass error to the error handler
}
});
// Error-handling middleware
app.use((err, req, res, next) => {
 console.error(err.stack);
 res.status(500).json({
 message: 'An error occurred',
 error: process.env.NODE_ENV === 'production' ? {} : err
 });
});

Handling Async Errors

For async middleware, make sure to catch promise rejections and pass them to next():

// Async middleware with proper error handling
app.get('/async-data', async (req, res, next) => {
 try {
 const data = await fetchDataFromDatabase();
 res.json(data);
 } catch (error) {
 next(error); // Pass error to the error handler
}
});
// Alternative using Express 4.16+ wrapper
function asyncHandler(fn) {
 return (req, res, next) => {
 Promise.resolve(fn(req, res, next)).catch(next);
 };
}
app.get('/better-async', asyncHandler(async (req, res) => {
 const data = await fetchDataFromDatabase();
 res.json(data);
}));

Note

Express 5 (currently in beta) will automatically catch Promise rejections and pass them to the error handler.

Middleware Execution Order

The order in which middleware is defined matters significantly.

Express executes middleware in the order they are added to the application.

Example: Order Matters

const express = require('express');
const app = express();
// This middleware will run first
app.use((req, res, next) => {
 console.log('First middleware');
 next();
});
// This middleware will run for /users paths only
app.use('/users', (req, res, next) => {
 console.log('Users middleware');
 next();
});
// This route handler will run when matched
app.get('/users', (req, res) => {
 res.send('Users list');
});
// This middleware will never run for successfully matched routes
// because route handlers end the request-response cycle
app.use((req, res, next) => {
 console.log('This will not run for matched routes');
 next();
});
// This is a "catch-all" middleware for unmatched routes
app.use((req, res) => {
 res.status(404).send('Not found');
});

Best practices for middleware order

  • Place middleware that applies to all requests first (logging, security, body parsing)
  • Place more specific middleware and routes next
  • Place error-handling middleware last

Example: Recommended Order

// 1. Application-wide middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(morgan('dev'));
app.use(helmet());
// 2. Route-specific middleware
app.use('/api', authenticate);
// 3. Routes
app.use('/api/users', userRoutes);
app.use('/api/products', productRoutes);
// 4. 404 handler
app.use((req, res) => {
 res.status(404).json({ message: 'Not found' });
});
// 5. Error handler (always last)
app.use((err, req, res, next) => {
 console.error(err);
 res.status(500).json({ message: 'Server error' });
});

Best Practices

Follow these best practices when working with middleware in Node.js:

Keep Middleware Focused

Each middleware should have a single responsibility, following the Single Responsibility Principle.

Use Next() Properly

  • Always call next() unless you're ending the response
  • Never call next() after sending a response
  • Call next() with an error parameter to trigger error handling

Handle Async Code Properly

Always catch errors in async middleware and pass them to next() .

Don't Overuse Middleware

Too many middleware functions can impact performance. Use them judiciously.

Previous

Node.js Express.js

Next

Node.js RESTful API