DEV Community

Cover image for Middleware in Express.js
mahdi
mahdi

Posted on • Edited on

Middleware in Express.js

Introduction:

Express.js, a popular web application framework for Node.js, provides a robust middleware system that allows developers to handle requests and responses effectively. In this article, we will dive deeper into middleware usage in Express.js, exploring the intricacies of both built-in and custom middleware. By the end of this journey, you'll have a comprehensive understanding of how to write custom middleware and seamlessly manage middleware chains in your Express.js applications.

Understanding Middleware: A Simple Analogy

Imagine you're taking a trip to a scorching desert. You know it's going to be hot, and you plan to stay for two hours. Now, you can survive the heat for that time, but to make the journey more comfortable and secure, you decide to bring along a small backpack.

In this backpack, you pack a water bottle (built-in middleware) to stay hydrated and a basic first aid kit (custom middleware) in case you need it. These extras are like your travel companions, providing support during your time in the desert.

In the same way, when your application makes a request to the server, you can attach certain "travel companions" (middleware) to handle specific aspects of the request, ensuring a smoother and more efficient journey.

Understanding Built-in Middleware

Internal middleware in Express.js refers to the middleware functions that are built into the Express framework itself and are available for use without requiring additional installations or third-party packages. These internal middleware functions are part of the core functionality provided by Express to handle common tasks and streamline the development process.

Simply put:

Express.js is like a smart butler for your website, and it comes with some built-in helpers or "middleware."

Think of middleware as tools the butler uses to handle different tasks. These tools are already in the butler's toolkit, so you don't need to go out and buy or make new ones.

For example:

  • JSON Tool: Helps the butler understand and work with information sent in a special format (JSON).
  • URL Decoder: Decodes information that comes in a different way (URL-encoded data).

These tools are like magical helpers the butler uses behind the scenes to keep things running smoothly. They're part of the butler's core skills, making your website work better without you needing to worry about finding or creating these tools yourself.

So, when you hear about internal middleware in Express.js, think of it as the butler's handy tools that come built-in, making your website tasks easier.

Well, after complete understanding, let's continue more!

  1. Purpose and Functionality:

    • Internal middleware functions in Express serve various purposes, such as parsing incoming data, handling sessions, enabling static file serving, and managing routing.
  2. Ease of Use:

    • Since internal middleware is included with Express, developers can easily leverage these functions by adding them to their application with minimal setup.
  3. Examples of Internal Middleware:

    • Body Parsing Middleware:
      • express.json(): Parses incoming JSON data from requests.
      • express.urlencoded(): Parses incoming URL-encoded data from forms.
    • Static File Middleware:
      • express.static(): Serves static files such as HTML, CSS, and images.
    • Session Middleware:
      • express-session: Enables session management in Express applications.
    • Error Handling Middleware:
      • Express has built-in mechanisms to handle errors in middleware and route handlers.
  4. Integration with Routes:

    • Internal middleware can be applied globally to the entire application or selectively to specific routes. This flexibility allows developers to control where and how each middleware function is utilized.
  5. Order of Middleware Execution:

    • The order in which internal middleware functions are applied matters. Middleware is executed in the order it's defined, influencing the request and response objects as they pass through each middleware function in the stack.
  6. Extensibility:

    • While internal middleware covers common use cases, Express is designed to be extensible. Developers can create custom middleware function1. Purpose and Functionality:
    • Internal middleware functions in Express serve various purposes, such as parsing incoming data, handling sessions, enabling static file serving, and managing routing.
  7. Ease of Use:

    • Since internal middleware is included with Express, developers can easily leverage these functions by adding them to their application with minimal setup.
  8. Examples of Internal Middleware:

    • Body Parsing Middleware:
      • express.json(): Parses incoming JSON data from requests.
      • express.urlencoded(): Parses incoming URL-encoded data from forms.
    • Static File Middleware:
      • express.static(): Serves static files such as HTML, CSS, and images.
    • Session Middleware:
      • express-session: Enables session management in Express applications.
    • Error Handling Middleware:
      • Express has built-in mechanisms to handle errors in middleware and route handlers.
  9. Integration with Routes:

    • Internal middleware can be applied globally to the entire application or selectively to specific routes. This flexibility allows developers to control where and how each middleware function is utilized.
  10. Order of Middleware Execution:

    • The order in which internal middleware functions are applied matters. Middleware is executed in the order it's defined, influencing the request and response objects as they pass through each middleware function in the stack.
  11. Exts to address specific needs not covered by the built-in middleware.

Example: Using Body Parsing Middleware



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

// Internal middleware for parsing JSON and URL-encoded data
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

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

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


Enter fullscreen mode Exit fullscreen mode

In this example, express.json() and express.urlencoded() are internal middleware functions that simplify the process of handling JSON and URL-encoded data in the application.

In summary, internal middleware in Express.js enhances developer productivity by providing pre-built functions for common tasks. This allows developers to focus on building application logic rather than writing boilerplate code for routine functionalities.


Well, now let's learn about customized middleware, how we can make a customized middleware ourselves!

Custom Middleware Functions

If we want to introduce customized middleware, I can say this:

In some projects, the express middleware may not meet our needs and this is where the custom middleware comes to our service like an assistant.

tip :
Before writing the custom middleware, you must define its task

An example for better understanding!

Imagine your Express.js application as a mysterious mansion with a secret room. To enter the room, you need a special key. The authentication middleware is like the guardian of the secret door checking if you possess the key. If you do, you're granted access; otherwise, you're denied entry.

Writing the Middleware (Guardian's Instructions):

  • The guardian (authentication middleware) has clear instructions to check for a special key (authorization token) before allowing entry.
  • If the key is correct, the guardian grants permission and marks the visitor (request) as having the key.


   function authenticationMiddleware(req, res, next) {
     // Check if the visitor (user) has the right key (authorization token)
     if (req.headers.authorization === 'secret-key') {
       req.hasKey = true; // Mark the visitor as having the key
       next(); // Allowed to enter the secret room
     } else {
       req.hasKey = false; // Mark the visitor as not having the key
       res.status(401).send('Access denied! You need the special key.');
     }
   }


Enter fullscreen mode Exit fullscreen mode

Implementing the Middleware (Guardian on Duty):

  • The guardian (authentication middleware) is stationed at the entrance of the mansion (implemented in your Express app).
  • Every visitor (request) approaching the secret room is checked by the guardian.


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

   app.use(authenticationMiddleware);

   // Secret room route - only accessible to those with the key
   app.get('/secret-room', (req, res) => {
     if (req.hasKey) {
       res.send('Welcome to the secret room!');
     } else {
       res.status(401).send('Access denied! You need the special key.');
     }
   });



Enter fullscreen mode Exit fullscreen mode

In this analogy, when a visitor (request) arrives at the mansion, the guardian (authentication middleware) checks if they have the special key (authorization token). If they do, the visitor is marked as having the key (req.hasKey = true), and they're allowed to access the secret room route. If they lack the key, the visitor is marked as not having the key (req.hasKey = false), and access is denied.

I think this was an understandable example!
If you do not understand, let me know! Or if you have a better example, share it with me!

If you, like me, did not understand the importance of the (next) function while learning, I must say that we are also in pain! But let's put it simply and also talk about their performance

Middleware Execution Flow

Imagine you are driving on a highway, and there are toll booths along the way. Each toll booth is like a middleware function, and the payment receipt is the control flow. The order of toll booths and passing the receipt determines how smoothly you can continue your journey.

Middleware Execution Flow:

  1. Entering the Highway (Start of the Journey):

    • You start your journey on the highway (enter your Express.js application).
  2. Toll Booth 1 (Middleware Function 1):

    • The first toll booth (middleware function) appears. You pay the toll (execute its task), and in return, you receive a receipt (control flow).
  3. Passing the Receipt:

    • The receipt is crucial. It indicates that you've paid at the first toll booth and can proceed to the next one.
  4. Toll Booth 2 (Middleware Function 2):

    • The second toll booth (middleware function) comes up. To pass through, you need to show the receipt (control flow) from the first booth.
    • After checking the receipt, the second toll booth accepts it and gives you a new receipt (control flow) for the next section.
  5. Continuing Through Toll Booths:

    • This process repeats for each toll booth (middleware function) along the highway.
  6. Reaching the Destination (Route Handler):

    • Finally, you reach your destination (the route handler) after successfully passing through all the toll booths with the necessary receipts (control flow).

So what's next??

In general, in the example we read together, it means that the receipt is like a key to pass through each counter, serving as proof that you successfully navigated the previous one.

In Express programs, this translates to when we say next(), it means proceed to the next step once you have completed what I asked you to do.

It's evident that we can't advance to the next counter without a receipt, just as we can't move to the next step in Express without calling next().

Additionally, the order of toll booths is crucial. If the sequence of the first and second booths changes, our entire program might come to a halt, potentially causing issues during our journey.


So what if we made a mistake?
Let's talk about error control in middleware:

Error Handling Middleware:

Role of Error Handling Middleware:
Error handling middleware is like a safety net in your Express.js application. Its job is to catch any errors that might happen during the processing of a request and respond in a way that makes sense, preventing your application from crashing unexpectedly.

Illustration of Creating Custom Error Handling Middleware:

In this example, let's create a simple Express.js app and a custom error handling middleware to handle errors.



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

// Custom Error Handling Middleware
const customErrorHandler = (err, req, res, next) => {
  // Log the error for debugging purposes
  console.error(err);

  // Respond with an appropriate message
  res.status(500).send('Oops! Something went wrong. Our team has been notified.');
};

// Applying the Error Handling Middleware
app.use(customErrorHandler);

// Example Route with Error
app.get('/example', (req, res, next) => {
  // Simulate an error (e.g., a database error)
  const error = new Error('Database connection failed');
  next(error);
});

// Start the server
const PORT = 3000;
app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
});


Enter fullscreen mode Exit fullscreen mode

Explanation:

  • The customErrorHandler is a middleware that catches errors. It logs the error for developers to see and responds with a generic message to the client to avoid exposing internal details.
  • In the example route /example, we intentionally simulate an error by creating a new Error object with the message 'Database connection failed' and passing it to the next middleware using next(error).

This way, if any unexpected errors occur during the processing of the /example route, the customErrorHandler will be there to catch them, log them for developers to see, and respond to the client with a friendly message. It's like having a safety net to handle issues and keep your application running smoothly.

Best Practices:

Break down complex middleware into smaller, focused functions. This makes your code more modular and easier to maintain. And also have better readability
Example:



// Middleware functions in separate modules

// loggingMiddleware.js
const loggingMiddleware = (req, res, next) => {
  console.log(`[${new Date()}] ${req.method} ${req.url}`);
  next();
};

// authenticationMiddleware.js
const authenticationMiddleware = (req, res, next) => {
  if (req.headers.authorization === 'valid-token') {
    next();
  } else {
    res.status(401).send('Unauthorized');
  }
};

// App.js
const loggingMiddleware = require('./middlewares/loggingMiddleware');
const authenticationMiddleware = require('./middlewares/authenticationMiddleware');

app.use(loggingMiddleware);
app.use('/secure-route', authenticationMiddleware);


Enter fullscreen mode Exit fullscreen mode

2. Error Handling Consistency:

Best Practice: Establish a consistent approach to error handling across your middleware. Use a centralized error handler for uniformity.

Example:



// Centralized error handling middleware

// errorHandlingMiddleware.js
const errorHandlingMiddleware = (err, req, res, next) => {
  console.error(err.stack);
  res.status(500).send('Internal Server Error');
};

// App.js
const errorHandlingMiddleware = require('./middlewares/errorHandlingMiddleware');

app.use(errorHandlingMiddleware);

// Example Route with Error
app.get('/example', (req, res, next) => {
  // Simulate an error (e.g., database error)
  const error = new Error('Database connection failed');
  next(error);
});


Enter fullscreen mode Exit fullscreen mode

These examples showcase how modularizing middleware functions into separate files and establishing a consistent error handling approach contribute to better organization and maintainability in your Express.js application.

Conclusion:

In conclusion, understanding and implementing middleware effectively in Express.js is a crucial aspect of building robust and scalable web applications. Throughout this article, we've explored various facets of middleware, including practical examples and best practices. Let's summarize the key points:

  1. Middleware Overview:

    • Middleware functions play a vital role in handling requests and responses in the Express.js framework.
    • They provide a way to execute code during the request-response cycle, allowing for customization and additional functionalities.
  2. Types of Middleware:

    • Explored built-in middleware like express.json() and express.urlencoded().
    • Discussed the importance of error handling middleware in gracefully managing errors during processing.
  3. Best Practices:

    • Emphasized best practices for writing efficient and maintainable middleware, such as modularization and consistent error handling.
    • Explored common pitfalls and provided examples on how to avoid them.
  4. Experimentation and Application:

    • Encouraged readers to experiment with custom middleware in their own projects.
    • Highlighted the flexibility and adaptability of middleware in tailoring Express.js applications to specific needs.

As you venture into building Express.js applications, consider the power and versatility that middleware provides. Experiment with custom middleware functions tailored to your application's requirements, and remember to follow best practices to ensure a clean and maintainable codebase.

Middleware is a key element in shaping the flow of your application, adding layers of functionality, and handling errors gracefully. By mastering middleware concepts and applying them judiciously, you'll be better equipped to build resilient and feature-rich web applications with Express.js

Connect and Collaborate:

Your input, feedback, and collaboration are highly valued! Feel free to initiate discussions, share your thoughts, or inquire about anything related to the topics covered in this article. I'm always open to engaging with the community. Connect with me through the following platforms:

I invite you to explore BaseShop, a recent project where I've implemented middleware. Your insights are valuable, and I welcome any suggestions, improvements, or even contributions through pull requests. Let's collaborate and make the development journey even more enriching!

Middleware plays a pivotal role in shaping the application's flow, enhancing functionality, and gracefully handling errors. By mastering middleware concepts and applying them strategically, we empower ourselves to build resilient and feature-rich web applications with Express.js
Middleware is a fundamental element in shaping the flow of your application, adding layers of functionality, and handling errors gracefully. By mastering middleware concepts and applying them judiciously, you'll be better equipped to build resilient and feature-rich web applications with Express.js

Top comments (3)

Collapse
 
onlinemsr profile image
Raja MSR

I particularly liked the way the you used examples to illustrate the different concepts. Covered wide range of middleware concepts, including error handling, logging, and authentication. I found the section on writing custom middleware to be especially helpful.

Collapse
 
m__mdy__m profile image
mahdi

Thank you very much. I use such examples in all my articles. I am proud that you liked the way I explained!

Collapse
 
610470416 profile image
NotFound404

You can embrace new javascript features and simplify your web development while still keeping compatibility with most expressjs middlewares with aex now.