CacheLane Logo

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

  1. Continue with the existing router class TrieRouter. Add new functionalities to it.

  2. 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.

  3. Modify the addRoute method of the TrieRouter class to take an additional parameter method.

  • method: A string representing the HTTP method. This could be "GET", "POST", "PUT", "DELETE", etc.
  1. 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.

  2. 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

  1. When you're adding or searching for a route, make sure to consider both the path and the HTTP method.

  2. Take care to handle the HTTP method case-insensitively (prefer uppercase). It's common to receive HTTP method names in different cases.

  3. Be careful with your error-handling logic to correctly manage the situation where the client does not provide a valid HTTP method.

  4. As with Challenge 1, start by making sure the Trie works for a simple case before diving into the more complex functionalities.

  5. 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.

Previous
Ex. Implementing our Trie based Router