## Ex. Query Parameters (Advanced)

<!-- Learn about query parameters in Javascript and Node.js, and implement a super-fast custom query parameter parser to parse and decode query parameters. -->

Query parameters, also known as `URL` parameters or `GET` parameters, are an important concept in HTTP based communication. They provide a way to pass data to a web server through the URL of a web page or any client application that's able to make `GET` requests. Query parameters are an essential part of the HTTP protocol and play a crucial role in creating dynamic and interactive web/server applications.

In this chapter, we'll dive deep into query parameters, exploring their structure, usage, and best practices. We'll also discuss their importance in modern web development and how they differ from other methods of data transmission.

### Anatomy of a URL with Query Parameters

Before we delve into the specifics of query parameters, let's break down the structure of a URL that includes them:

```js
https://www.cachelane.com/path/to/page?param1=value1&param2=value2
```

This URL can be divided into several components:

- **Protocol**: `https://`
- **Domain**: `www.cachelane.com`
- **Path**: `/path/to/page`
- **Query string**: `?param1=value1&param2=value2`

The query string begins with a question mark (`?`) and contains the query parameters. Each parameter is a key-value pair, where the key and value are separated by an equals sign (`=`). Multiple parameters are separated by ampersands (`&`).

#### Key Components of Query Parameters

**Question Mark (?)**:

- Signals the start of the query string.
- Separates the path from the query parameters

**Parameter Name**:

- The key in the key-value pair
- Typically descriptive of the data it represents

**Equals Sign (=)**:

- Separates the parameter name from its value

**Parameter Value**:

- The actual data being passed
- Can be a string, number, or even a more complex data structure (when properly encoded)

**Ampersand (&)**:

- Used to separate multiple parameters in the query string. For example, `?param1=value1&param2=value2` contains two parameters - `param1` and `param2`, with the corresponding values `value1` and `value2` respectively.

### Challenge 1: Implementing Basic Query Parameter Parsing

In this challenge, we'll enhance our `Router` class to parse and handle basic query parameters. We'll focus on extracting query parameters from the URL and making them available to route handlers.

Modify the `Router` class to parse query parameters from incoming URLs and include them in the route matching result.

#### Requirements

1. Update the `findRoute` method to separate the path from query parameters.
2. Implement a new method `parseQueryParams` to extract query parameters from the URL.
3. Include the parsed query parameters in the object returned by `findRoute`.
4. Ensure that routes without query parameters still work as before.
5. **Important**: Handle URL encoding and decoding properly. Learn more about [URL encoding](https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding).

#### Hints

1. You may use the `URL` or `URLSearchParams` class in Node.js to parse query parameters.
2. Don't worry about the performance yet, just make sure that your logic works correctly.
3. Use the current `Router` class implementation as your starting point. You'll need to modify the `findRoute` method and add a new `parseQueryParams` method.

#### Expected Outcome

After implementing this feature, the `findRoute` method should return an object that includes:

- `params`: Dynamic route parameters (as before)
- `query`: An object containing parsed query parameters
- `handler`: The matched route handler (as before)

#### A quick heads up

You may need to change the implementation of the `findRoute` method, and update how it's extracting the path params. For example, you might notice that if the path parameter is at the end of the URL, the current implementation will not work as expected. You may need to update the implementation to handle such cases.

#### Example

```javascript
const router = new Router();
router.get("/users/:id", (req, res) => {
  console.log(req.params.id);
  console.log(req.query);
});

const result = router.findRoute("/users/123?name=Velocy&age=1", "GET");
console.log(result);
// Expected output:
// {
//   params: { id: '123' },
//   query: { name: 'Velocy', age: '1' },
//   handler: [Function]
// }

// Actual output:
// {
//   params: { id: '123?name=Velocy&age=1' },
//   query: { name: 'velocy', age: '1' },
//   handler: [Function]
// }
```

#### Solution

Here's the basic solution I came up with for this challenge:

```js
// file: lib/router.js
class Router {
  ...

  /**
   * Parse query parameters from the URL
   * @param {string} queryString - URL segment containing query parameters
   * @returns {object} - Parsed query parameters as key-value pairs
   */
  #parseQueryParams(queryString) {
    if (!queryString) return {};
    // Extract the query string from the URL

    const queryParams = {};
    // Split the query string into key-value pairs
    const pairs = queryString.split("&");

    for (const pair of pairs) {
      // Split each pair into key and value
      const [key, value] = pair.split("=");
      if (key && value) {
        // Decode the value and store the key-value pair
        queryParams[key] = decodeURIComponent(value);
      }
    }

    return queryParams;
  }
 ...
}
```

The updated `findRoute` method:

```diff
findRoute(path, method) {
+    const indexOfDelimiter = path.indexOf("?");
+    let _path, querySegment;
+
+    if (indexOfDelimiter !== -1) {
+      _path = path.substring(0, indexOfDelimiter);
+      querySegment = path.substring(indexOfDelimiter + 1);
+    } else {
+      _path = path;
+    }
+
+    let segments = _path.split("/").filter(Boolean);
-    let segments = path.split("/").filter(Boolean);

     ...

+    let query = querySegment ? this.#parseQueryParams(querySegment) : {};
    return {
      params,
+      query,
      handler: currentNode.handler.get(method),
    };
  }
```

The updated `run` method inside `lib/index.js`

```diff
// file: lib/index.js

function run(router, port) {
  ...

  createServer(function _create(req, res) {
    const route = router.findRoute(req.url, req.method);

    if (route?.handler) {
      req.params = route.params || {};
+    req.query = route.query || {};
      route.handler(req, res);
    } else {
      ...
    }
  }).listen(port);
}
```

#### Explanation

```js
// file lib/index.js
function run(router, port) {
  createServer(function _create(req, res) {
    ... unchanged

    if (route?.handler) {
      req.params = route.params || {};
      req.query = route.query || {}; // added this line
      route.handler(req, res);
    } else {
      ...
    }

    ... unchanged
  })
}
req.query = route.query || {};
```

The very first thing we need to make sure is that the `run` method in `lib/index.js` is updated to include the `query` object in the `req` object passed to the route handler.

That way, the users of our library can access the query parameters in their route handlers using `req.query`.

```js
findRoute(path, method) {
  // Separate path from query parameters
  const indexOfDelimiter = path.indexOf("?");
  let _path, querySegment;

  // If query parameters exist, split the URL into path and query segments
  if (indexOfDelimiter !== -1) {
    _path = path.substring(0, indexOfDelimiter);
    querySegment = path.substring(indexOfDelimiter + 1);
  } else {
    _path = path;
  }
  ...
}
```

First, we need to separate the path from the query parameters. We do this by finding the position of the `?` character in the URL using `path.indexOf("?")`. If the `?` character is found, ie. `path.indexOf("?") !== -1`, we split the URL into `_path` (the part before `?`) and `querySegment` (the part after `?`). If the `?` character is not found, `_path` is set to the entire URL, and `querySegment` remains undefined.

```js
let query = querySegment ? this.#parseQueryParams(querySegment) : {};
```

At the end of the `findRoute` method, we need to parse the query parameters if they exist. We check if `querySegment` is defined. If it is, we call `this.#parseQueryParams(querySegment)` to parse the query parameters. If it is not, we set `query` to an empty object `{}`.

```js
return {
  params,
  query,
  handler: currentNode.handler.get(method),
};
```

Finally, we include the parsed query parameters in the object returned by the `findRoute` method. We add the `query` object to the return value, alongside `params` and `handler`.

Now, let's break down the `#parseQueryParams` method step by step.

```js
if (!queryString) return {};
```

Next, we check if the `queryString` is empty. If it is, we return immediately since there are no query parameters to parse.

```js
const queryParams = {};
const pairs = queryString.split("&");
```

We then initialize an empty object `queryParams` to store the parsed key-value pairs. We split the `queryString` by the `&` character to get individual key-value pairs. Each pair represents a single query parameter.

```js
for (const pair of pairs) {
  const [key, value] = pair.split("=");

  if (key && value) {
    queryParams[key] = decodeURIComponent(value);
  }
}

return queryParams;
```

We iterate over each `pair` in the `pairs` array. For each pair, we split it by the `=` character to separate the key and value and check if both `key` and `value` are present. If they are, we decode the value using `decodeURIComponent` to handle URL encoding and store the key-value pair in the `queryParams` object. For example, `name=Velocy%20Framework` would be parsed as `{ name: 'Velocy Framework' }`.

Finally, we return the `queryParams` object containing all the parsed query parameters.

---

#### Let's test our implementation

```js
❯ node test.js
{
  params: [Object: null prototype] { id: '123' },
  query: { name: 'John', age: '30' },
  handler: [Function (anonymous)]
}
```

Everything looks good.

### We need to tackle more edge cases

The current implementation of the `#parseQueryParams` method works well for basic query parameters. However, it doesn't handle more complex scenarios, such as:

- Special characters in parameter names and values
- URL-encoded characters in both the key and value parts
- Efficiency and performance optimizations by avoiding unnecessary string allocations

### Challenge 2: Parsing Query Parameters Manually

In this challenge, we'll take our query parameter parsing to the next level by implementing it manually without relying on built-in Node.js classes like `URL` or `URLSearchParams`, or using the `decodeURIComponent` function. This will give you a deeper understanding of how query parameter parsing works under the hood.

#### Requirements

- Improve our `parseQueryParams` method that takes a query string and returns an object with parsed parameters.
- Handle multiple parameters, including those with the same name. For now, you can overwrite the value if a parameter appears multiple times.
- Properly decode URL-encoded characters in both parameter names and values.
- Handle edge cases such as empty values, missing values, and special characters.
- Integrate this custom parsing function into your `Router` class.
- Remember to handle URL decoding for both keys and values.

#### Expected Outcome

Your custom `parseQueryParams` function should be able to handle a variety of query strings and return an object with correctly parsed parameters. The `Router` class should use this function to populate the `query` property in the object returned by `findRoute`.

#### Example

```javascript
function parseQueryParams(queryString) {
  // Your implementation here
}

// Test cases
console.log(parseQueryParams("name=John&age=30"));
// Expected: { name: 'John', age: '30' }

console.log(parseQueryParams("color=red&color=blue&color=green"));
// Expected: { color: 'green' } // any one value, based on what you want - the first or the last one.

console.log(parseQueryParams("my%20message=Hello%20World%21&empty=&noval"));
// Expected: { 'my message': 'Hello World!', empty: '', noval: '' }

// Integration with Router class
const router = new Router();
router.get("/search", (req, res) => {
  console.log(req.query);
});

const result = router.findRoute("/search?q=nodejs&limit=10", "GET");
console.log(result.query);
// Expected: { q: 'nodejs', limit: '10' }
```

#### Solution

The very first thing that I did, was to create a new file `lib/utils.js` and created an object that maps all the special characters to their respective values.

```js
// file lib/utils.js

// prettier-ignore
const encodedMap = { "3A": ":", "2F": "/", "3F": "?", 23: "#", "5B": "[", "5D": "]", 40: "@", 21: "!", 24: "$", 26: "&", 27: "'", 28: "(", 29: ")", "2A": "*", "2B": "+", "2C": ",", "3B": ";", "3D": "=", 25: "%", 20: " ", 22: '"', "2D": "-", "2E": ".", 30: "0", 31: "1", 32: "2", 33: "3", 34: "4", 35: "5", 36: "6", 37: "7", 38: "8", 39: "9", 41: "A", 42: "B", 43: "C", 44: "D", 45: "E", 46: "F", 47: "G", 48: "H", 49: "I", "4A": "J", "4B": "K", "4C": "L", "4D": "M", "4E": "N", "4F": "O", 50: "P", 51: "Q", 52: "R", 53: "S", 54: "T", 55: "U", 56: "V", 57: "W", 58: "X", 59: "Y", "5A": "Z", 61: "a", 62: "b", 63: "c", 64: "d", 65: "e", 66: "f", 67: "g", 68: "h", 69: "i", "6A": "j", "6B": "k", "6C": "l", "6D": "m", "6E": "n", "6F": "o", 70: "p", 71: "q", 72: "r", 73: "s", 74: "t", 75: "u", 76: "v", 77: "w", 78: "x", 79: "y", "7A": "z", "5E": "^", "5F": "_", 60: "`", "7B": "{", "7C": "|", "7D": "}", "7E": "~", }; // prettier-ignore
```

Here's a new decoder function that I created to parse query parameters manually, using the `encodedMap` object:

```js
// file lib/utils.js

let encodedMap = {...}

function fastDecode(string) {
  let result = "";
  let lastIndex = 0;
  let index = string.indexOf("%");

  while (index !== -1) {
    result += string.substring(lastIndex, index);
    const hexVal = string.substring(index + 1, index + 3);
    result += encodedMap[hexVal] || "%" + hexVal;
    lastIndex = index + 3;
    index = string.indexOf("%", lastIndex);
  }

  return result + string.substring(lastIndex);
 }

```

Here are the changes that I made to the `parseQueryParams` method:

```js
// file lib/router.js

// Added a default value for queryString to handle empty strings
// and more performance improvements
#parseQueryParams(queryString = "") {
  if (!queryString) return {};

  const queryParams = {};
  const pairs = queryString.split("&");

  for (const pair of pairs) {
    const splitPair = pair.split("=");
    let key = splitPair[0];
    let value = splitPair[1] || "";

    if (key.indexOf("%") !== -1) {
      key = fastDecode(key);
    }

    if (value.indexOf("%") !== -1) {
      value = fastDecode(value);
    }

    if (key) {
      queryParams[key] = value;
    }
  }

  return queryParams;
}
```

#### Explanation

The `fastDecode` function is an optimized method for decoding URL-encoded strings. Let's break down this function and examine why and how it works

```javascript
// Define the function that takes a string as input
function fastDecodeAll(string) {
  // Initialize an empty result string
  let result = "";

  // Set the initial lastIndex to 0
  let lastIndex = 0;

  // Find the first occurrence of '%' in the string
  let index = string.indexOf("%");

  // More code follows...
}
```

This function initializes key variables: `result` to store the decoded string, `lastIndex` to keep track of the last processed index, and `index` to find encoded characters.

##### Main Decoding Loop

```javascript
// Continue loop while '%' is found in the string
while (index !== -1) {
  // Add the substring from lastIndex to current index to the result
  result += string.substring(lastIndex, index);

  // Extract the two characters following '%'
  const hexVal = string.substring(index + 1, index + 3);

  // Look up the decoded value in encodedMap or keep original if not found
  result += encodedMap[hexVal] || "%" + hexVal;

  // Update lastIndex to skip the processed encoded character
  lastIndex = index + 3;

  // Find the next '%' in the string
  index = string.indexOf("%", lastIndex);
}
```

This is the core of the decoding process. It efficiently handles encoded characters by:

Appending unencoded substrings directly to the result.
Using a pre-computed `encodedMap` for quick lookups of decoded values.
Updating indices to skip processed parts of the string.

**Efficiency**: This approach is more efficient than our previous solution for several reasons:

- It avoids repeated string allocations by using a single `result` string.
- It uses a pre-computed map for constant-time lookups of decoded values.
- It processes the string in a single pass, reducing time complexity.

##### Handling the Remainder

```javascript
// Add any remaining unprocessed part of the string to the result
return result + string.substring(lastIndex);
```

This final step ensures that any remaining unencoded portion of the string is included in the result.

#### Explanation of `#parseQueryParams`

##### Function Declaration and Initial Check

```javascript
// Define the function with a default empty string parameter
#parseQueryParams(queryString = "") {
  // Return an empty object if queryString is falsy
  if (!queryString) return {};

  // Initialize an empty object to store parsed query parameters
  const queryParams = {};
```

This part of the code remains largely unchanged. The function now has a default parameter of an empty string, which is a minor improvement in handling edge cases.

##### Splitting the Query String

```javascript
// Split the query string into key-value pairs
const pairs = queryString.split("&");
```

This line splits the query string into an array of key-value pairs. It's identical in both versions and has a time complexity of O(n), where n is the length of the query string.

##### Parsing Individual Pairs

```javascript
  for (const pair of pairs) {

    // Split each pair into key and value
    const splitPair = pair.split("=");

    // Assign the key (always exists)
    let key = splitPair[0];

    // Assign the value (may be undefined, default to empty string)
    let value = splitPair[1] || "";
```

This section shows is almost simpler, but the main difference is that we're not destructuring the `splitPair` array directly into `key` and `value`. Instead, we're assigning them individually.

##### Decoding Key and Value

```javascript
// Check if key needs decoding
if (key.indexOf("%") !== -1) {
  key = fastDecode(key);
}

// Check if value needs decoding
if (value.indexOf("%") !== -1) {
  value = fastDecode(value);
}
```

This is the most crucial change in the new implementation. Instead of always using `decodeURIComponent`, it first checks if decoding is necessary by looking for the '%' character. If found, it uses a custom `fastDecode` (not shown in the snippet) to decode the string.

This approach is more efficient for several reasons:

1. It avoids unnecessary decoding operations when the string doesn't contain encoded characters.
2. The custom `fastDecode` can be optimized for the specific use case, potentially outperforming the generic `decodeURIComponent`.

##### Assigning to Query Params Object

```javascript
// Add the key-value pair to the queryParams object if key exists
if (key) {
  queryParams[key] = value;
}
```

This part is similar to the original version, but it doesn't need to check if the value exists because we've already defaulted it to an empty string if it was undefined.

##### Returning the Result

```javascript
  // Return the object containing all parsed query parameters
  return queryParams;
}
```

Here's a quick overview of the changes made to the `#parseQueryParams` method:

```diff
  for (const pair of pairs) {
-   const [key, value] = pair.split("=");
-   if (key && value) {
-     queryParams[key] = decodeURIComponent(value);
-   }
+   const splitPair = pair.split("=");
+   let key = splitPair[0];
+   let value = splitPair[1] || "";
+
+   if (key.indexOf("%") !== -1) {
+     key = fastDecode(key);
+   }
+
+   if (value.indexOf("%") !== -1) {
+     value = fastDecode(value);
+   }
+
+   if (key) {
+     queryParams[key] = value;
+   }
  }
```

> Note: The `fastDecode` function here is still not the complete implementation. You may need to add more characters to the `encodedMap` object to handle all possible encoded characters.

#### Conclusion

Query parameters are a powerful and flexible tool, which allow for the transmission of data through URLs, enabling dynamic and interactive client-side applications. By understanding their structure, usage, and best practices, we can create more efficient, user-friendly, and secure web applications.

Remember to always consider security implications when working with query parameters, and choose the most appropriate method of data transmission based on your specific use case and requirements.
