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;
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: [],
},
},
};
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: [],
},
},
};
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 theroutes
array or if the route has aname
we'll check if its name exists in thenamedRoutes
array. -
except
in this one, we'll see if our current request route not exists in theroutes
list nor innamedRoutes
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);
}
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;
}
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.
- Collect middlewares from
all
list. - Collect middlewares from
only
list. - Collect middlewares from
except
list. - 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;
}
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) {
//
}
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);
}
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",
});
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;
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
- Event Driven Architecture: A Practical Guide in Javascript
- Best Practices For Case Styles: Camel, Pascal, Snake, and Kebab Case In Node And Javascript
- After 6 years of practicing MongoDB, Here are my thoughts on MongoDB vs MySQL
Packages & Libraries
- Collections: Your ultimate Javascript Arrays Manager
- Supportive Is: an elegant utility to check types of values in JavaScript
- Localization: An agnostic i18n package to manage localization in your project
React Js Packages
Courses (Articles)
Top comments (0)