diff --git a/Readme.md b/Readme.md index 23cd841..b587b32 100644 --- a/Readme.md +++ b/Readme.md @@ -25,13 +25,14 @@ The repo for our backend framework- [Velocy](https://github.com/ishtms/velocy) [![Read Next](/assets/imgs/next.png)](/chapters/ch01-what-is-a-web-server-anyway.md) # Table of contents -- [(optional) Node.js is way faster than you think](/chapters/ch00-nodejs-faster-than-you-think.md) - - [Contenders for the test](/chapters/ch00-nodejs-faster-than-you-think.md#contenders-for-the-test) - - [Elysia - Bun](/chapters/ch00-nodejs-faster-than-you-think.md#elysia---bun) - - [Axum - Rust](/chapters/ch00-nodejs-faster-than-you-think.md#axum---rust) - - [Express - Node.js](/chapters/ch00-nodejs-faster-than-you-think.md#express---nodejs) - - [Velocy - Node.js](/chapters/ch00-nodejs-faster-than-you-think.md#velocy---nodejs) - - [The benchmark](/chapters/ch00-nodejs-faster-than-you-think.md#the-benchmark) + +- [(optional) Node.js is way faster than you think](/chapters/ch00-nodejs-faster-than-you-think.md) + - [Contenders for the test](/chapters/ch00-nodejs-faster-than-you-think.md#contenders-for-the-test) + - [Elysia - Bun](/chapters/ch00-nodejs-faster-than-you-think.md#elysia---bun) + - [Axum - Rust](/chapters/ch00-nodejs-faster-than-you-think.md#axum---rust) + - [Express - Node.js](/chapters/ch00-nodejs-faster-than-you-think.md#express---nodejs) + - [Velocy - Node.js](/chapters/ch00-nodejs-faster-than-you-think.md#velocy---nodejs) + - [The benchmark](/chapters/ch00-nodejs-faster-than-you-think.md#the-benchmark) - [What the hell is a web server any way?](/chapters/ch01-what-is-a-web-server-anyway.md) - [Parts of a Web Server](/chapters/ch01-what-is-a-web-server-anyway.md#parts-of-a-web-server) - [Navigating the World of Protocols: A Quick Overview](/chapters/ch01-what-is-a-web-server-anyway.md#navigating-the-world-of-protocols-a-quick-overview) @@ -251,5 +252,8 @@ The repo for our backend framework- [Velocy](https://github.com/ishtms/velocy) - [Arrow functions are not free](/chapters/ch06.2-the-router-class.md#arrow-functions-are-not-free) - [Why should we care about memory?](/chapters/ch06.2-the-router-class.md#why-should-we-care-about-memory) - [Testing the updated code](/chapters/ch06.2-the-router-class.md#testing-the-updated-code) +- [Improving the `Router` API](/chapters/ch06.3-improving-the-router-api.md) +- [The need for a `trie`](/chapters/ch06.4-the-need-for-a-trie.md) + - [What is a `Trie` anyway?](/chapters/ch06.4-the-need-for-a-trie.md#what-is-a-trie-anyway) ![](https://uddrapi.com/api/img?page=readme) diff --git a/chapters/.md b/chapters/.md new file mode 100644 index 0000000..e69de29 diff --git a/chapters/ch06.1-basic-router-implementation.md b/chapters/ch06.1-basic-router-implementation.md index 8a1812b..430f41b 100644 --- a/chapters/ch06.1-basic-router-implementation.md +++ b/chapters/ch06.1-basic-router-implementation.md @@ -438,11 +438,11 @@ A general rule of thumb is - Your functions should only do what they are suppose function add(x, y) { // Not only adding x and y, but also writing to console, which is not expected. console.log(`Adding ${x} and ${y}`); - + // Performing a file operation, which is definitely not expected from an 'add' function. const fs = require('fs'); fs.writeFileSync('log.txt', `Adding ${x} and ${y}\n`, { flag: 'a+' }); - + // Sending an HTTP request, which is out of scope for an 'add' function. const http = require('http'); const data = JSON.stringify({ result: x + y }); diff --git a/chapters/ch06.3-improving-the-router-api.md b/chapters/ch06.3-improving-the-router-api.md index d604b10..c7d0886 100644 --- a/chapters/ch06.3-improving-the-router-api.md +++ b/chapters/ch06.3-improving-the-router-api.md @@ -1,3 +1,5 @@ +[![Read Prev](/assets/imgs/prev.png)](/chapters/ch06.2-the-router-class.md) + ## Improving the `Router` API The utility method on the `Router` class - `addRoute` is a bit too verbose. You need to specify the HTTP method as a string. It would get tedious when there are suppose hundreds of API routes in an application. Also, devs might not know whether the HTTP methods should be sent in lower-case or upper-case without looking at the source. @@ -93,7 +95,7 @@ Let's go through the new additions in our code: ```js get(path, handler) { - this.#addRoute(HTTP_METHODS.GET, path, handler); +    this.#addRoute(HTTP_METHODS.GET, path, handler); } post(path, handler) { @@ -116,6 +118,26 @@ this.#addRoute("GET", path, handler); There's nothing wrong with this approach too, but I prefer avoiding to use raw strings. `"GET"` can mean many things, but `HTTP_METHODS.GET` gives us the actual idea of what it is all about. +Let's update our testing code to call the newly created http methods instead: + +```js +// file: index.js + +... + +router.get("/", function handleGetBasePath(req, res) { + console.log("Hello from GET /"); + res.end(); +}); + +router.post("/", function handlePostBasePath(req, res) { + console.log("Hello from POST /"); + res.end() +}); + +... +``` + If we do a quick test on both the endpoints, every thing seems to be working alright: ```bash @@ -138,4 +160,8 @@ $ curl http://localhost:5255/foo -v # Not found ``` +Great! This looks much better than the previous implementation. + +[![Read Next](/assets/imgs/next.png)](/chapters/ch06.4-the-need-for-a-trie.md) + ![](https://uddrapi.com/api/img?page=ch6.3) diff --git a/chapters/ch06.4-the-need-for-a-trie.md b/chapters/ch06.4-the-need-for-a-trie.md new file mode 100644 index 0000000..544aabd --- /dev/null +++ b/chapters/ch06.4-the-need-for-a-trie.md @@ -0,0 +1,106 @@ +[![Read Prev](/assets/imgs/prev.png)](/chapters/ch06.3-improving-the-router-api.md) + +## The Need for a `Trie` + +Until now, we've been using a straightforward object to store our routes. While this is simple and easy to understand, it's not the most efficient way to store routes, especially when we have a large number of them or when we introduce dynamic routing capabilities like `/users/:id`. It's a simple and readable approach but lacks efficiency and the capability for dynamic routing. As we aim to build a robust, scalable, and high-performance backend framework, it is crucial to optimize our routing logic. + +As long as you don't need dynamic parameters, or query parameters, you'd be good enough with a javascript object (like we do now), or a `Map`. But a backend framework that doesn't supports dynamic parameters, or query parsing is as good as a social media site without an ability to add friends. + +In this chapter, we'll explore a new data-structure that you may not have heard of before - **Trie**. We'll also look at how we can utilize it to enhance our router's performance. + +For example, imagine we have the following four routes: + +```bash +GET /api/v1/accounts/friend +GET /api/v1/accounts/stats +GET /api/v1/accounts/upload +GET /api/v1/accounts/blocked_users +POST /api/v1/accounts/friend +POST /api/v1/accounts/stats +POST /api/v1/accounts/upload +POST /api/v1/accounts/blocked_users +``` + +Our current implementation will have them stored as separate keys in the object: + +```json +{ + "GET /api/v1/accounts/friend": function handle_friend() { ... }, + "GET /api/v1/accounts/stats": function handle_stats() { ... }, + "GET /api/v1/accounts/upload": function handle_upload() { ... }, + "GET /api/v1/accounts/blocked_users": function handle_blocked_users() { ... }, + "POST /api/v1/accounts/friend": function handle_friend() { ... }, + "POST /api/v1/accounts/stats": function handle_stats() { ... }, + "POST /api/v1/accounts/upload": function handle_upload() { ... }, + "POST /api/v1/accounts/blocked_users": function handle_blocked_users() { ... } +} +``` + +That is not efficient. For most of the applications this is nothing to worry about, but there's a better way. Also with this approach it becomes impossible to extend our router with other functionalities like we talked above - dynamic routes, queries etc. There's a way to do some regex sorcery to achieve it, but that method will be way way slower. You don't need to sacrifice performance in order to support more features. + +A better way to store the routes could be the following: + +```json +{ + "/api": { + "/v1": { + "/accounts": { + "friend": function handle_friend() { ... }, + "stats": function handle_stats() { ... }, + "upload": function handle_upload() { ... }, + "blocked_users": function handle_blocked_users() { ... } + } + } + } +} +``` + +This is an easy way to think of how a `Trie` stores the paths. + +### What is a `Trie` anyway? + +A `Trie` which is also known as a prefix tree, is a specialized tree structure used for storing a mapping between keys and values, where the keys are generally strings. This structure is organized in such a way that all the child nodes that stem from a single parent node have a shared initial sequence of characters, or a "common prefix." So the position of a node in the Trie dictates what key it corresponds to, rather than storing the key explicitly in the node itself. + +Imagine we have the following routes: + +```bash +'GET /users' +'GET /users/id' +'POST /users' +``` + +With our current implementation, the routes object would look like: + +```json +{ + "GET /users": handler, + "GET /users/id": handler, + "POST /users": handler +} +``` + +But, with a Trie, it will look like the following: + +```bash + [root] + | + GET + | + users + / \ + POST GET + \ + id +``` + +Every node, including `root` will be an object that contain some necessary information with it. + +1. `handler`: The function to be executed when the route represented by the path to this node is accessed. Not all nodes will have handlers, only the nodes that correspond to complete routes. + +2. `path`: The current route segment in string, for example - `/users` or `/id` + +3. `param` and `paramName`: If the current path is `/:id` and the client makes a request at `/xyz`, the `param` will be `xyz` and the `paramName` will be `id`. + +4. `children`: Any children nodes. (We'll get more deep into this in the upcoming chapters) + +Enough with the theory. In the next chapter, we'll dive into our very first exercise for this book: **implementing a Trie**.