DEV Community

Cover image for 70-Nodejs Course 2023: Super Middlewares
Hasan Zohdy
Hasan Zohdy

Posted on

70-Nodejs Course 2023: Super Middlewares

Lately, we've introduced a new concept called Global Middlewares (i'll call it GM), which is a very useful concept, it allows us to apply one or more middlewares to all the routes, now let's see how to make it more specific, how to apply middlewares to specific routes.

Specific middlewares for specific routes

Now we've implemented GM in our app, but we want to expand it more.

We can split the middleware into three types:

  • all - applies to all routes
  • only - applies to specific routes
  • except - applies to all routes except specific routes

Let's see how to implement it.

Updating Http Configurations

We have created the http config file and added middleware property to it, we will now split it into three properties:

// src/config/http.ts
const httpConfigurations = {
  middleware: {
    // apply the middleware to all routes
    all: [],
    // apply the middleware to specific routes
    only: {
      routes: [],
      middleware: [],
    },
    // exclude the middleware from specific routes
    except: {
      routes: [],
      middleware: [],
    },
  },
};

export default httpConfigurations;
Enter fullscreen mode Exit fullscreen mode

Here we updated the middleware to be an object, and we added three properties to it:

  • all - applies to all routes, so we just pass list of middlewares to it.
  • only - applies to specific routes, so we pass the list of routes and the list of middlewares to it.
  • except - applies to all routes except specific routes, so we pass the list of routes and the list of middlewares to it.

Named Routes

Remember, we added long time ago name property to route options and i told you we'll use it later, now its time to use it.

Purpose of Named Routes

We can use the name property to identify the route, and we can use it to apply middlewares to specific routes.

So if we wanted later to change the route path, we can do it easily, and we don't need to change the middleware configuration.

So an example of usage might be something like:

// src/config/http.ts
const httpConfigurations = {
  middleware: {
    // apply the middleware to all routes
    all: [],
    // apply the middleware to specific routes
    only: {
      routes: ['auth.login', 'auth.register'],
      middleware: [],
    },
    // exclude the middleware from specific routes
    except: {
      routes: ['auth.logout', 'auth.refresh'],
      middleware: [],
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

And so on, you know what, actually i just thought why not add both features, routes and namedRoutes, so we can use both of them.

// src/config/http.ts
const httpConfigurations = {
  middleware: {
    // apply the middleware to all routes
    all: [],
    // apply the middleware to specific routes
    only: {
      routes: ['login', '/register'],
      namedRoutes: ['auth.login', 'auth.register'],
      middleware: [],
    },
    // exclude the middleware from specific routes
    except: {
      routes: ['/logout', '/refresh-token'],
      namedRoutes: ['auth.logout', 'auth.refresh'],
      middleware: [],
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

Now we've a lot of work to do, so let's get started quickly.

Upgrading Middlewares

Now let's move our middleware to V2, earlier we had made it just an array of middlewares, now le'ts update it to match our new cases

Upgrade workflow

So how this is going to work? let's see

We've three cases here:

  • The all which is the simplest, we'll just merge all middlewares in that array with current route middlewares.
  • only which we'll check for the route if exists in the routes array or if the route has a name we'll check if its name exists in the namedRoutes array.
  • except in this one, we'll see if our current request route not exists in the routes list nor in namedRoutes in that case we'll merge with current route middlewares, otherwise skip its middlewares.

Let's implement it

This is the current executeMiddleware method:

// src/core/http/request.ts
// ..
  /**
   * Execute middleware list of current route
   */
  protected async executeMiddleware() {
    // get route middlewares
    const routeMiddlewares = this.route.middleware || [];
    // get global middlewares
    const globalMiddlewares = config.get("http.middleware", []);

    // merge global and route middlewares
    const middlewares = [...globalMiddlewares, ...routeMiddlewares];

    // check if there are no middlewares, then return
    if (middlewares.length === 0) return;

    // trigger the executingMiddleware event
    this.trigger("executingMiddleware", middlewares, this.route);

    for (const middleware of middlewares) {
      const output = await middleware(this, this.response);

      if (output !== undefined) {
        this.trigger("executedMiddleware");
        return output;
      }
    }

    // trigger the executedMiddleware event
    this.trigger("executedMiddleware", middlewares, this.route);
  }
Enter fullscreen mode Exit fullscreen mode

Instead of making the method bigger, we can actually make another method just to collect middlewares list for current route, let's call it collectMiddlewares

// src/core/http/request.ts
// ...

  /**
   * Collect middlewares for current route
   */
  protected collectMiddlewares(): Middleware[] {
    const middlewaresList: Middleware[] = [];

    return middlewaresList;
  }
Enter fullscreen mode Exit fullscreen mode

We just defined the method and set its return type to return list of middlewares, inside it i only defined an empty array and returned it.

Now let's write our steps for collecting these middlewares, we also need to make it ordered so priority will work accordingly.

Lower priority means it will be executed lastly, for example all middlewares will have highest priority thus it will be executed before any other middlewares

We will collect middlewares from high to priority.

  1. Collect middlewares from all list.
  2. Collect middlewares from only list.
  3. Collect middlewares from except list.
  4. Collect middlewares from current route, this will have the least priority

So the order for execution will go from 1 to 4 in the previous list, let's see it.

// src/core/http/request.ts
// ...

  /**
   * Collect middlewares for current route
   */
  protected collectMiddlewares(): Middleware[] {
    // we'll collect middlewares from 4 places
    // We'll collect from http configurations under `http.middleware` config
    // it has 3 middlewares types, `all` `only` and `except`
    // and the final one will be the middlewares in the route itself
    // so the order of collecting and executing will be: `all` `only` `except` and `route`
    const middlewaresList: Middleware[] = [];

    // 1- collect all middlewares as they will be executed first
    const allMiddlewaresConfigurations = config.get("http.middleware.only");

    // check if it has middleware list
    if (allMiddlewaresConfigurations?.middleware) {
      // now just push everything there
      middlewaresList.push(...allMiddlewaresConfigurations.middleware);
    }

    // 2- check if there is `only` property
    const onlyMiddlewaresConfigurations = config.get("http.middleware.only");

    if (onlyMiddlewaresConfigurations?.middleware) {
      // check if current route exists in the `routes` property
      // or the route has a name and exists in `namedRoutes` property
      if (
        onlyMiddlewaresConfigurations.routes?.includes(this.route.path) ||
        (this.route.name &&
          onlyMiddlewaresConfigurations.namedRoutes?.includes(this.route.name))
      ) {
        middlewaresList.push(...onlyMiddlewaresConfigurations.middleware);
      }
    }

    // 3- collect routes from except middlewares
    const exceptMiddlewaresConfigurations = config.get(
      "http.middleware.except",
    );

    if (exceptMiddlewaresConfigurations?.middleware) {
      // first check if there is `routes` property and route path is not listed there
      // then check if route has name and that name is not listed in `namedRoutes` property
      if (
        !exceptMiddlewaresConfigurations.routes?.includes(this.route.path) &&
        this.route.name &&
        !exceptMiddlewaresConfigurations.namedRoutes?.includes(this.route.name)
      ) {
        middlewaresList.push(...exceptMiddlewaresConfigurations.middleware);
      }
    }

    // 4- collect routes from route middlewares
    if (this.route.middleware) {
      middlewaresList.push(...this.route.middleware);
    }

    return middlewaresList;
  }
Enter fullscreen mode Exit fullscreen mode

The code is pretty much self explained, but let me focus on multiple things here:

Firstly, let's note that is used optional chaining feature in mostly every check, this saves me writing the same code, for example i wrote onlyMiddlewaresConfigurations?.middleware this is equivalent to the following

if (onlyMiddlewaresConfigurations?.middleware) {
  //
}

  // is the same as 
if (onlyMiddlewaresConfigurations && onlyMiddlewaresConfigurations.middleware) {
  //
}
Enter fullscreen mode Exit fullscreen mode

Secondly, as the method is a little complex and more ambiguous, i wrote multiple inline comments so if if forgot what is going there i can remember from my comments, or even better, if your mate or boss is reading your code, he/she will be so delightful.

Furthermore, i continued writing comments above every middleware collector so i know what i'm going to do in that if statement.

Now let's update our executeMiddleware method to use collectMiddlewares instead of just the route middlewares.

// src/core/http/request.ts
// ...
  /**
   * Execute middleware list of current route
   */
  protected async executeMiddleware() {
    // collect all middlewares for current route
    const middlewares = this.collectMiddlewares();

    // check if there are no middlewares, then return
    if (middlewares.length === 0) return;

    // trigger the executingMiddleware event
    this.trigger("executingMiddleware", middlewares, this.route);

    for (const middleware of middlewares) {
      const output = await middleware(this, this.response);

      if (output !== undefined) {
        this.trigger("executedMiddleware");
        return output;
      }
    }

    // trigger the executedMiddleware event
    this.trigger("executedMiddleware", middlewares, this.route);
  }
Enter fullscreen mode Exit fullscreen mode

Now let's try our named routes as we didn't even try it when we added that feature.

// src/app/users/routes.ts
// ...
router.get("/users", usersList, {
  // add the route name
  name: "users.list",
});

Enter fullscreen mode Exit fullscreen mode

I removed the middleware from the users list and added the route name, i prefer to use the module name as a prefix then the state of the route, or what is it going to do, so i named it users.list.

Now let's use it with only for example

// src/config/http.ts
import { authMiddleware } from "core/auth/auth-middleware";

const httpConfigurations = {
  middleware: {
    // apply the middleware to all routes
    all: [],
    // apply the middleware to specific routes
    only: {
      routes: [],
      namedRoutes: ["users.list"],
      middleware: [authMiddleware("user")],
    },
    // exclude the middleware from specific routes
    except: {
      routes: [],
      namedRoutes: [],
      middleware: [],
    },
  },
};

export default httpConfigurations;
Enter fullscreen mode Exit fullscreen mode

Now if we tried the request in postman, it should only work if the current user is logged in.

If you want to make sure this works fine, change the user to `guest argument in authMiddleware function, you should see then "error": "You are not allowed to access this resource"

🎨 Conclusion

We made a good progress in middlewares we added multiple ways to define middlewares either locally and globally, in our next article, we're going to introduce our new route feature, the group method.

β˜•β™¨οΈ Buy me a Coffee β™¨οΈβ˜•

If you enjoy my articles and see it useful to you, you may buy me a coffee, it will help me to keep going and keep creating more content.

πŸš€ Project Repository

You can find the latest updates of this project on Github

😍 Join our community

Join our community on Discord to get help and support (Node Js 2023 Channel).

🎞️ Video Course (Arabic Voice)

If you want to learn this course in video format, you can find it on Youtube, the course is in Arabic language.

πŸ“š Bonus Content πŸ“š

You may have a look at these articles, it will definitely boost your knowledge and productivity.

General Topics

Packages & Libraries

React Js Packages

Courses (Articles)

Top comments (0)