Skip to content

Commit 00f50b5

Browse files
committed
adds chapter 6.3 & chapter 6.4
1 parent 26fbfd2 commit 00f50b5

File tree

5 files changed

+146
-10
lines changed

5 files changed

+146
-10
lines changed

Readme.md

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,14 @@ The repo for our backend framework- [Velocy](https://github.com/ishtms/velocy)
2525
[![Read Next](/assets/imgs/next.png)](/chapters/ch01-what-is-a-web-server-anyway.md)
2626

2727
# Table of contents
28-
- [(optional) Node.js is way faster than you think](/chapters/ch00-nodejs-faster-than-you-think.md)
29-
- [Contenders for the test](/chapters/ch00-nodejs-faster-than-you-think.md#contenders-for-the-test)
30-
- [Elysia - Bun](/chapters/ch00-nodejs-faster-than-you-think.md#elysia---bun)
31-
- [Axum - Rust](/chapters/ch00-nodejs-faster-than-you-think.md#axum---rust)
32-
- [Express - Node.js](/chapters/ch00-nodejs-faster-than-you-think.md#express---nodejs)
33-
- [Velocy - Node.js](/chapters/ch00-nodejs-faster-than-you-think.md#velocy---nodejs)
34-
- [The benchmark](/chapters/ch00-nodejs-faster-than-you-think.md#the-benchmark)
28+
29+
- [(optional) Node.js is way faster than you think](/chapters/ch00-nodejs-faster-than-you-think.md)
30+
- [Contenders for the test](/chapters/ch00-nodejs-faster-than-you-think.md#contenders-for-the-test)
31+
- [Elysia - Bun](/chapters/ch00-nodejs-faster-than-you-think.md#elysia---bun)
32+
- [Axum - Rust](/chapters/ch00-nodejs-faster-than-you-think.md#axum---rust)
33+
- [Express - Node.js](/chapters/ch00-nodejs-faster-than-you-think.md#express---nodejs)
34+
- [Velocy - Node.js](/chapters/ch00-nodejs-faster-than-you-think.md#velocy---nodejs)
35+
- [The benchmark](/chapters/ch00-nodejs-faster-than-you-think.md#the-benchmark)
3536
- [What the hell is a web server any way?](/chapters/ch01-what-is-a-web-server-anyway.md)
3637
- [Parts of a Web Server](/chapters/ch01-what-is-a-web-server-anyway.md#parts-of-a-web-server)
3738
- [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)
251252
- [Arrow functions are not free](/chapters/ch06.2-the-router-class.md#arrow-functions-are-not-free)
252253
- [Why should we care about memory?](/chapters/ch06.2-the-router-class.md#why-should-we-care-about-memory)
253254
- [Testing the updated code](/chapters/ch06.2-the-router-class.md#testing-the-updated-code)
255+
- [Improving the `Router` API](/chapters/ch06.3-improving-the-router-api.md)
256+
- [The need for a `trie`](/chapters/ch06.4-the-need-for-a-trie.md)
257+
- [What is a `Trie` anyway?](/chapters/ch06.4-the-need-for-a-trie.md#what-is-a-trie-anyway)
254258

255259
![](https://uddrapi.com/api/img?page=readme)

chapters/.md

Whitespace-only changes.

chapters/ch06.1-basic-router-implementation.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -438,11 +438,11 @@ A general rule of thumb is - Your functions should only do what they are suppose
438438
function add(x, y) {
439439
// Not only adding x and y, but also writing to console, which is not expected.
440440
console.log(`Adding ${x} and ${y}`);
441-
441+
442442
// Performing a file operation, which is definitely not expected from an 'add' function.
443443
const fs = require('fs');
444444
fs.writeFileSync('log.txt', `Adding ${x} and ${y}\n`, { flag: 'a+' });
445-
445+
446446
// Sending an HTTP request, which is out of scope for an 'add' function.
447447
const http = require('http');
448448
const data = JSON.stringify({ result: x + y });

chapters/ch06.3-improving-the-router-api.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
[![Read Prev](/assets/imgs/prev.png)](/chapters/ch06.2-the-router-class.md)
2+
13
## Improving the `Router` API
24

35
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:
9395

9496
```js
9597
get(path, handler) {
96-
this.#addRoute(HTTP_METHODS.GET, path, handler);
98+
    this.#addRoute(HTTP_METHODS.GET, path, handler);
9799
}
98100

99101
post(path, handler) {
@@ -116,6 +118,26 @@ this.#addRoute("GET", path, handler);
116118

117119
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.
118120

121+
Let's update our testing code to call the newly created http methods instead:
122+
123+
```js
124+
// file: index.js
125+
126+
...
127+
128+
router.get("/", function handleGetBasePath(req, res) {
129+
console.log("Hello from GET /");
130+
res.end();
131+
});
132+
133+
router.post("/", function handlePostBasePath(req, res) {
134+
console.log("Hello from POST /");
135+
res.end()
136+
});
137+
138+
...
139+
```
140+
119141
If we do a quick test on both the endpoints, every thing seems to be working alright:
120142

121143
```bash
@@ -138,4 +160,8 @@ $ curl http://localhost:5255/foo -v
138160
# Not found
139161
```
140162

163+
Great! This looks much better than the previous implementation.
164+
165+
[![Read Next](/assets/imgs/next.png)](/chapters/ch06.4-the-need-for-a-trie.md)
166+
141167
![](https://uddrapi.com/api/img?page=ch6.3)
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
[![Read Prev](/assets/imgs/prev.png)](/chapters/ch06.3-improving-the-router-api.md)
2+
3+
## The Need for a `Trie`
4+
5+
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.
6+
7+
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.
8+
9+
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.
10+
11+
For example, imagine we have the following four routes:
12+
13+
```bash
14+
GET /api/v1/accounts/friend
15+
GET /api/v1/accounts/stats
16+
GET /api/v1/accounts/upload
17+
GET /api/v1/accounts/blocked_users
18+
POST /api/v1/accounts/friend
19+
POST /api/v1/accounts/stats
20+
POST /api/v1/accounts/upload
21+
POST /api/v1/accounts/blocked_users
22+
```
23+
24+
Our current implementation will have them stored as separate keys in the object:
25+
26+
```json
27+
{
28+
"GET /api/v1/accounts/friend": function handle_friend() { ... },
29+
"GET /api/v1/accounts/stats": function handle_stats() { ... },
30+
"GET /api/v1/accounts/upload": function handle_upload() { ... },
31+
"GET /api/v1/accounts/blocked_users": function handle_blocked_users() { ... },
32+
"POST /api/v1/accounts/friend": function handle_friend() { ... },
33+
"POST /api/v1/accounts/stats": function handle_stats() { ... },
34+
"POST /api/v1/accounts/upload": function handle_upload() { ... },
35+
"POST /api/v1/accounts/blocked_users": function handle_blocked_users() { ... }
36+
}
37+
```
38+
39+
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.
40+
41+
A better way to store the routes could be the following:
42+
43+
```json
44+
{
45+
"/api": {
46+
"/v1": {
47+
"/accounts": {
48+
"friend": function handle_friend() { ... },
49+
"stats": function handle_stats() { ... },
50+
"upload": function handle_upload() { ... },
51+
"blocked_users": function handle_blocked_users() { ... }
52+
}
53+
}
54+
}
55+
}
56+
```
57+
58+
This is an easy way to think of how a `Trie` stores the paths.
59+
60+
### What is a `Trie` anyway?
61+
62+
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.
63+
64+
Imagine we have the following routes:
65+
66+
```bash
67+
'GET /users'
68+
'GET /users/id'
69+
'POST /users'
70+
```
71+
72+
With our current implementation, the routes object would look like:
73+
74+
```json
75+
{
76+
"GET /users": handler,
77+
"GET /users/id": handler,
78+
"POST /users": handler
79+
}
80+
```
81+
82+
But, with a Trie, it will look like the following:
83+
84+
```bash
85+
[root]
86+
|
87+
GET
88+
|
89+
users
90+
/ \
91+
POST GET
92+
\
93+
id
94+
```
95+
96+
Every node, including `root` will be an object that contain some necessary information with it.
97+
98+
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.
99+
100+
2. `path`: The current route segment in string, for example - `/users` or `/id`
101+
102+
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`.
103+
104+
4. `children`: Any children nodes. (We'll get more deep into this in the upcoming chapters)
105+
106+
Enough with the theory. In the next chapter, we'll dive into our very first exercise for this book: **implementing a Trie**.

0 commit comments

Comments
 (0)