A couple of days ago I started to build a Node.Js backend without any 3rd party dependencies and today I'm going to explain how I developed a router for that application.
Before going further I would like to tell you that this is the first time I'm doing this type of project and the logic behind the router might not be the most efficient or the best solution. So I'd like to invite you to share your thoughts with me and others.
So basically when you consider a route, you can break it down into a couple of components.
- Subroutes and path parameters
- Query parameters
First, let's see how we can develop a logic for subroutes and path parameters.
You can think of a route as a tree data structure where each route will have a set of sub-routes with or without a wild card(path parameter).
Configure a path
Initially, we have to separate query params and the path as follows.
let endpoint;
if (req.url.includes('?')) {
endpoint = req.url.split('?')[0];
} else endpoint = req.url;
Now before we add/configure an endpoint, we have to separate each subroutes using '/'.
if (endpoint[endpoint.length - 1] != '/') endpoint += '/';
let path = endpoint.split('/');
If we splice path '/someroute/another' using the above algorithm we get an output as follows
['', 'someroute', 'another', '']
For every route, we are getting the ''
route at the beginning. So for every route, we can ignore it.
let adjPath = path.slice(1, path.length);
Next, we can set an iterator to the adjPath
array and go from each level to the next level adding each part of the path to the tree.
We can identify wildcards using the ':' in front of it. So we can set that path as a wild card.
static constructPath(path, currIndex, head) {
if (currIndex === path.length) {
return;
}
const newObj = new Router(path[currIndex]);
if (path[currIndex][0] === ':') {
head.wildCard = newObj;
this.constructPath(path, currIndex + 1, head.wildCard);
} else {
const childrenAvailable = head.children.find(
(c) => c.path === path[currIndex]
);
if (childrenAvailable) {
const childrenIndex = head.children.findIndex(
(c) => c.path === path[currIndex]
);
this.constructPath(
path,
currIndex + 1,
head.children[childrenIndex]
);
} else {
head.addChildren(newObj);
this.constructPath(
path,
currIndex + 1,
head.children[head.children.length - 1]
);
}
}
}
While configuring the given path, we have to check whether the current subroute is already configured or not. If it's already configured, then we can just continue to the next section of the route.
By repeating the above steps we can set up a tree data structure for the routes of our application.
Run controllers upon requests
Except for constructing the tree in the above section, we have to keep track of which endpoint to run on which route. We can do it by simply setting an object with adjPath
variable as the key.
Before we run the controller, we have to check whether the requested path is valid or not and find its format (with path parameters). We can do it simply by searching the tree data structure.
Before that again we have to convert the requested path to an array as above.
Then for each subroute, we can check whether it includes in the tree or whether a wildcard exists. If both are false, we can send a 404 response saying that the requested path is not available.
static getPath(path, currIndex, head) {
if (currIndex === path.length) {
return [];
}
const pathExists = head.children.find(
(p) => p.path === path[currIndex]
);
let res;
if (pathExists) {
const tempRes = this.getPath(path, currIndex + 1, pathExists);
if (!tempRes) return null;
res = [pathExists.path, ...tempRes];
} else if (head.wildCard) {
const tempRes = this.getPath(path, currIndex + 1, head.wildCard);
if (!tempRes) return null;
res = [head.wildCard.path, ...tempRes];
} else return null;
return res;
}
After that, we can compare differences between the returned value from the getPath function and the array which contains the path and name those different items as path parameters.
Ex: constructedPath = ['someroute', ':param', 'end', '']
reqPath = ['someroute', 'paramValue', 'end', '']
let params = {};
for (let i = 0; i < constructedPath.length; i++) {
if (reqPath[i] !== constructedPath[i]) {
params = {...params,[constructedPath[i].substring(1,constructedPath[i].length)]: reqPath[i],};
}
}
From that, we can find the value for path parameter 'param' and it is 'paramValue'.
Finally, we can find query parameters from the URL by splitting it at '?' and implementing the following algorithm.
let query = {};
const splittedQuery = req.url.split('?');
if (splittedQuery.length == 2) {
const queryParams = splittedQuery[1].split('&');
queryParams.forEach((q) => {
const n = q.split('=');
query = { ...query, [n[0]]: n[1] };
});
}
So that's how I implement a router for my third-party dependency-free node.js application.
For more information and how I implemented the above routing strategies checkout here
Thank You!
Top comments (0)