We're still in the middle of events, but we need to introduce a new concept first before we move on as we need it in our request events, The concept is called Middleware
.
Middleware
Middleware is a function or list of functions that is executed before the request handler, it can be used to modify the request object, or to add some functionality to the request.
For example, we can use a middleware to check if the user is authenticated or not, or we can use it to check if the user has the required permissions to access the requested route, if not then we can stop the request and return a response error.
Request Middleware
Request middleware is a middleware that is executed before the request handler, it can be used to modify the request object, or to add some functionality to the request, or stop the request and return a response error/success based on certain conditions.
Request Middleware Implementation
Now let's start implementing the request middleware, if you recall, the router methods accepts two parameters, the first one is the route path, and the second one is the request handler, so we need to add a third parameter to the router methods, that one will be an options list, and we will add the middleware
option to it.
Open src/core/router/types.ts
and add let's update the Route type.
// src/core/router/types.ts
import { Request } from "core/http/request";
/**
* Middleware method
*/
export type Middleware = (request: Request, response: any) => any;
export type RouteOptions = {
/**
* Route middleware
*/
middleware?: Middleware[];
/**
* Route name
*/
name?: string;
};
/**
* Route Object
*/
export type Route = RouteOptions & {
/**
* Route method
*/
method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
/**
* Route path
*/
path: string;
/**
* Route handler
*/
handler: any;
};
We defined the Middleware
type, and we added the middleware
option to the RouteOptions
type.
Any middleware will receive the request object and the response object as parameters, if the middleware returns a value, then it will be used as the response, and the request handler and all other middlewares will not be executed.
Also we added a new option to the RouteOptions
type, that one is the name
option, it will be used to give a name to the route, we will use it later in the request events.
Finally we merged the RouteOptions
type with the Route
type.
Now let's update our Router class.
// src/core/router/index.ts
import request from "core/http/request";
import { Route, RouteOptions } from "./types";
export class Router {
/**
* Routes list
*/
private routes: Route[] = [];
/**
* Router Instance
*/
private static instance: Router;
/**
* Get router instance
*/
public static getInstance() {
if (!Router.instance) {
Router.instance = new Router();
}
return Router.instance;
}
private constructor() {
//
}
/**
* Add get request method
*/
public get(path: string, handler: any, options: RouteOptions = {}) {
this.routes.push({
method: "GET",
path,
handler,
...options,
});
return this;
}
/**
* Add post request method
*/
public post(path: string, handler: any, options: RouteOptions = {}) {
this.routes.push({
method: "POST",
path,
handler,
...options,
});
return this;
}
/**
* Add put request method
*/
public put(path: string, handler: any, options: RouteOptions = {}) {
this.routes.push({
method: "PUT",
path,
handler,
...options,
});
return this;
}
/**
* Add delete request method
*/
public delete(path: string, handler: any, options: RouteOptions = {}) {
this.routes.push({
method: "DELETE",
path,
handler,
...options,
});
return this;
}
/**
* Add patch request method
*/
public patch(path: string, handler: any, options: RouteOptions = {}) {
this.routes.push({
method: "PATCH",
path,
handler,
...options,
});
return this;
}
/**
* Get all routes list
*/
public list() {
return this.routes;
}
/**
* Register routes to the server
*/
public scan(server: any) {
this.routes.forEach(route => {
const requestMethod = route.method.toLowerCase();
const requestMethodFunction = server[requestMethod].bind(server);
requestMethodFunction(route.path, this.handleRoute(route));
});
}
/**
* Handle the given route
*/
private handleRoute(route: Route) {
return async (fastifyRequest: any, fastifyResponse: any) => {
request
.setRequest(fastifyRequest)
.setResponse(fastifyResponse)
.setHandler(route.handler);
return await request.execute();
};
}
}
const router = Router.getInstance();
export default router;
We added the options
parameter to the router methods, and we merged it with the route object.
Now let's update the handleRoute
method to receive the entire route object, and to call the execute
method on the request object.
// src/core/router/index.ts
private handleRoute(route: Route) {
return async (fastifyRequest: any, fastifyResponse: any) => {
request
.setRequest(fastifyRequest)
.setResponse(fastifyResponse)
.setRoute(route);
return await request.execute();
};
}
Now let's update our Request class to receive the route object instead of the handler.
// src/core/http/request.ts
import { Route } from "core/router/types";
export class Request {
// ...
/**
* Route Object
*
* Notice the `!` after the `Route` type, it means that the route is required.
*/
private route!: Route;
// remove the handler property
/**
* Set route handler
*/
public setRoute(route: Route) {
this.route = route;
return this;
}
// remove the setHandler method
We replaced the handler
property with the route
property, and we added the setRoute
method and removed the setHandler
method.
Now let's update the execute
method to call the middleware and the handler.
// src/core/http/request.ts
/**
* Execute the request
*/
public async execute() {
// check for middleware first
if (this.route.middleware) {
for (const middleware of this.route.middleware) {
const output = await middleware(this, this.response);
if (output !== undefined) {
return output;
}
}
}
const handler = this.route.handler;
// check for validation
if (handler.validation) {
if (handler.validation.rules) {
const validator = new Validator(this, handler.validation.rules);
try {
await validator.scan(); // start scanning the rules
} catch (error) {
console.log(error);
}
if (validator.fails()) {
const responseErrorsKey = config.get(
"validation.keys.response",
"errors",
);
const responseStatus = config.get("validation.responseStatus", 400);
return this.response.status(responseStatus).send({
[responseErrorsKey]: validator.errors(),
});
}
}
if (handler.validation.validate) {
const result = await handler.validation.validate(this, this.response);
if (result) {
return result;
}
}
}
return await handler(this, this.response);
}
In the execute method, we started by checking if the route has middleware, and if it does, we loop through the middleware and call each one of them, and if the middleware returns a value, we return it.
Then we check if the route has validation, and if it does, we check if the route has rules, and if it does, we create a new validator instance and we scan the rules, and if the validator fails, we return the errors.
Then we check if the route has a validation function, and if it does, we call it, and if it returns a value, we return it.
Finally, we call the handler and return the result.
And that's it!
Usage
Now let's see how to use it in our application.
Open src/app/users/route.ts
and update it to the following:
// src/app/users/route.ts
import router from "core/router";
import { Middleware } from "core/router/types";
import createUser from "./controllers/create-user";
import getUser from "./controllers/get-user";
import usersList from "./controllers/users-list";
const middleware1: Middleware = (request, response) => {
console.log("middleware 1 is executed but not returning anything");
};
const middleware2: Middleware = (request, response) => {
console.log(
"middleware 2 is executed but returning a value that will stop the execution and return the value",
);
return {
interrupted: true,
};
};
router.get("/users", usersList);
router.get("/users/:id", getUser);
router.post("/users", createUser, {
middleware: [middleware1, middleware2],
});
Now try to make a POST request to /users
, it will not execute even the validation, because the middleware returned a value.
Also you'll see something like this in your console:
🎨 Conclusion
In this tutorial, we learned how to implement middleware in our application, and how to use it.
In our next article, we'll resume our events on requests.
🚀 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)