Chapter 06
Ex. Adding HTTP method support
So far, we've built a router capable of matching URL paths to specific handlers. This is a good starting point, but as of now, our router does not differentiate between different HTTP methods like GET, POST, PUT, DELETE, etc. In real-world applications, the same URL path can behave differently based on the HTTP method used, making our current router almost useless for such scenarios.
Requirements
To make our router more useful and versatile, we need to extend the existing TrieRouter
and RouteNode
classes to support different HTTP methods (GET, POST, PUT, DELETE, etc.). This means that each node in the Trie could potentially have multiple handler functions, one for each HTTP method.
More details
Continue with the existing router class
TrieRouter
. Add new functionalities to it.The key in the
handler
Map
will be the HTTP method as a string (like "GET", "POST") and the value will be the handler function for that HTTP method.Modify the
addRoute
method of theTrieRouter
class to take an additional parametermethod
.
method
: A string representing the HTTP method. This could be "GET", "POST", "PUT", "DELETE", etc.
Also update the
findRoute
method. Now it will have another parameter -method
, to search for routes based on the HTTP method as well as the path.If a handler for a specific path and HTTP method is already present, the new handler should override the old one.
Example
Once implemented, the usage might look like this:
const trieRouter = new TrieRouter();
function getHandler() {}
function postHandler() {}
trieRouter.addRoute("/home", "GET", getHandler);
trieRouter.addRoute("/home", "POST", postHandler);
console.log(trieRouter.findRoute("/home", "GET")); // -> fn getHandler() {..}
console.log(trieRouter.findRoute("/home", "PATCH")); // -> null or undefined
console.log(trieRouter.findRoute("/home", "POST")); // -> fn postHanlder() {..}
Go ahead and add the functionality to our TrieRouter
class. This will involve making a lot of changes to the previous code. Feel free to share your implementation or ask for feedback in the Github discussions section.
Hints
When you're adding or searching for a route, make sure to consider both the path and the HTTP method.
Take care to handle the HTTP method case-insensitively (prefer uppercase). It's common to receive HTTP method names in different cases.
Be careful with your error-handling logic to correctly manage the situation where the client does not provide a valid HTTP method.
As with Challenge 1, start by making sure the Trie works for a simple case before diving into the more complex functionalities.
Don't forget to update your utility functions and other methods to be compatible with these new requirements.
Solution
Here's the solution I came up with:
const HTTP_METHODS = {
GET: "GET",
POST: "POST",
PUT: "PUT",
DELETE: "DELETE",
PATCH: "PATCH",
HEAD: "HEAD",
OPTIONS: "OPTIONS",
CONNECT: "CONNECT",
TRACE: "TRACE",
};
class RouteNode {
constructor() {
this.children = new Map();
this.handler = new Map();
}
}
class TrieRouter {
constructor() {
this.root = new RouteNode();
}
addRoute(path, method, handler) {
if (typeof path !== "string" || path[0] !== "/") throw new Error("Malformed path provided.");
if (typeof handler !== "function") throw new Error("Handler should be a function");
if (!HTTP_METHODS[method]) throw new Error("Invalid HTTP Method");
let currentNode = this.root;
let routeParts = path.split("/").filter(Boolean);
for (let idx = 0; idx < routeParts.length; idx++) {
const segment = routeParts[idx].toLowerCase();
if (segment.includes(" ")) throw new Error("Malformed `path` parameter");
let childNode = currentNode.children.get(segment);
if (!childNode) {
childNode = new RouteNode();
currentNode.children.set(segment, childNode);
}
currentNode = childNode;
}
currentNode.handler.set(method, handler); // Changed this line
}
findRoute(path, method) {
let segments = path.split("/").filter(Boolean);
let currentNode = this.root;
for (let idx = 0; idx < segments.length; idx++) {
const segment = segments[idx];
let childNode = currentNode.children.get(segment);
if (childNode) {
currentNode = childNode;
} else {
return null;
}
}
return currentNode.handler.get(method); // Changed this line
}
printTree(node = this.root, indentation = 0) {
/** Unchanged **/
}
}
The new HTTP method implementation introduces only some minor key changes to extend the existing router implementation to support HTTP methods. Below are the details of what was changed and why:
const HTTP_METHODS = {
GET: "GET",
POST: "POST",
PUT: "PUT",
DELETE: "DELETE",
PATCH: "PATCH",
HEAD: "HEAD",
OPTIONS: "OPTIONS",
CONNECT: "CONNECT",
TRACE: "TRACE",
};
Firstly, we've defined a constant object named HTTP_METHODS
to represent the different HTTP methods. This serves as a reference for the HTTP methods that our TrieRouter
class will support. We might even do some validation, but that is not necessary (we'll look at it in a later chapter why validation isn't required here)
class TrieRouter {
addRoute(path, method, handler) { ... }
...
}
In our TrieRouter
class, we updated the addRoute
method. It now takes an additional argument, method
, which specifies the HTTP method for the route.
addRoute(path, method, handler) {
if (typeof path !== "string" || path[0] !== "/") throw new Error("Malformed path provided.");
if (typeof handler !== "function") throw new Error("Handler should be a function");
// New check for HTTP method
if (!HTTP_METHODS[method]) throw new Error("Invalid HTTP Method");
...
}
The error handling has been updated to ensure the method
is a valid HTTP method.
this.handler = new Map();
The handler
in RouteNode
has changed from a single function reference to a Map
. This allows you to store multiple handlers for the same path but with different HTTP methods.
addRoute(path, method, handler) {
...
// Previous -> currentNode.handler = handler;
currentNode.handler.set(method, handler);
}
findRoute(path, method) {
...
// Previous -> return currentNode.handler;
return currentNode.handler.get(method); // Changed this line
}
In both the addRoute
and findRoute
methods, we've updated the line that sets and gets the handler for a specific path. Now, the handler is stored in the handler
map of the current node, with the HTTP method as the key.