Skip to content

Commit 8238a34

Browse files
committed
adds chapter 06.11 - Buildling a web server
1 parent 41b1f62 commit 8238a34

File tree

20 files changed

+825
-2
lines changed

20 files changed

+825
-2
lines changed

Readme.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ The repo for our backend framework- [Velocy](https://github.com/ishtms/velocy).
100100
- [`for..of`](chapters/ch03-working-with-files.md#-for-of-)
101101
- [`for await..of`](chapters/ch03-working-with-files.md#-for-await-of-)
102102
- [Reading the `json` file](chapters/ch03-working-with-files.md#reading-the-json-file)
103-
- [Buffers](chapters/ch03-working-with-files.md#buffers)
103+
- [Buffers](chapters/ch03-working-with-files.md#buffers)
104104
- [Parsing the `json` file](chapters/ch03-working-with-files.md#parsing-the-json-file)
105105
- [`logtar` our own logging library](chapters/ch04-logtar-our-logging-library.md#-logtar-our-own-logging-library)
106106
- [Initializing a new project](chapters/ch04-logtar-our-logging-library.md#initializing-a-new-project)
@@ -266,4 +266,7 @@ The repo for our backend framework- [Velocy](https://github.com/ishtms/velocy).
266266
- [Refactoring the `TrieRouter` class](chapters/ch06.10-running-our-server.md#refactoring-the-trierouter-class)
267267
- [Type Aliases](chapters/ch06.10-running-our-server.md#type-aliases)
268268
- [The `run` function](chapters/ch06.10-running-our-server.md#the-run-function)
269+
- [Building our first web-server](chapters/ch06.11-building-a-web-server.md#building-our-first-web-server)
270+
- [More refactoring](chapters/ch06.11-building-a-web-server.md#more-refactoring)
271+
- [Your first web server](chapters/ch06.11-building-a-web-server.md#your-first-web-server)
269272

chapters/ch03-working-with-files.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -849,7 +849,7 @@ Strangely, this outputs some weird looking stuff
849849

850850
Why is it so? And what is a `Buffer`? This is one of the most unvisited topics of programming. Let’s take a minute to understand it.
851851

852-
# Buffers
852+
## Buffers
853853

854854
`Buffer` objects are used to represent a fixed-length sequence of bytes, in memory. **`Buffer`** objects are more memory-efficient compared to JavaScript strings when dealing with data, especially very large datasets. This is because strings in JavaScript are UTF-16 encoded, which can lead to higher memory consumption for certain types of data.
855855

chapters/ch06.10-running-our-server.md

+8
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ function run(router, port) {
150150
const route = router.findRoute(req.url, req.path);
151151

152152
if (route?.handler) {
153+
req.params = route.params;
153154
route.handler(req, res);
154155
} else {
155156
res.writeHead(404, null, { "content-length": 9 });
@@ -195,6 +196,7 @@ We're creating an HTTP server using the `createServer` function. To re-iterate,
195196
const route = router.findRoute(req.url, req.path);
196197

197198
if (route?.handler) {
199+
req.params = route.params;
198200
route.handler(req, res);
199201
} else {
200202
res.writeHead(404, null, { "Content-Length": 9 });
@@ -204,8 +206,14 @@ if (route?.handler) {
204206
205207
We're calling the `findRoute` method on the `router` object to find the route that matches the incoming request. The `findRoute` method will return an object with two properties: `handler` and `params`. If a route is found, we'll call the `handler` function with the `req` and `res` objects. If no route is found, we'll return a `404 Not Found` response.
206208
209+
Inside the `if` statement, we're attaching a new property `req.params` to the `req` object. This property will contain the parameters extracted from the URL. The client can easily access the parameters using `req.params`.
210+
207211
You might have noticed that we're using a hard-coded `Content-Length` header with a value of `9`. This is because, if we do not specify the `Content-Length` header, the response headers will include a header `Transfer-Encoding: chunked`, which has a performance impact. We discussed about this in a previous chapter - [Chunks, oh no!](chapters/ch06.01-basic-router-implementation.md#chunks-oh-no-)
208212
209213
That's it! We have implemented the `run` function, which will allow us to run our server and listen for incoming requests. In the next chapter, we'll implement a simple server using our `Router` class and the `run` function.
210214
211215
> \*The callback function has multiple overloads, i.e it has a couple more function signatures. But for now, we're only interested in the one that takes a single callback function.
216+
217+
```
218+
219+
```
+335
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
## Building our first web-server
2+
3+
Our `Router` implementation has enough functionality to handle basic HTTP requests. In this chapter, we're going to spin up our first web server with this little toy `Router` and put it to test.
4+
5+
### More refactoring
6+
7+
Till now, our entire `Router` implementation and the helper function stayed in a single file. As we're going to build a web server, it's a good idea to separate the `Router` implementation into its own module, as well as the helper functions.
8+
9+
Here's the updated file structure:
10+
11+
```plaintext
12+
 ./
13+
├──  lib/ # Our library code
14+
│ ├──  constants.js # Constants used in our library
15+
│ ├──  index.js # Entry point of our library
16+
│ └──  router.js # Router implementation
17+
├──  globals.js # Global Typedefs
18+
└──  test.js # We'll write our code for testing here.
19+
```
20+
21+
#### `lib/router.js`
22+
23+
```js
24+
const { HTTP_METHODS } = require("./constants");
25+
26+
class RouteNode {
27+
constructor() {
28+
/** @type {Map<String, RouteNode>} */
29+
this.children = new Map();
30+
31+
/** @type {Map<String, RequestHandler>} */
32+
this.handler = new Map();
33+
34+
/** @type {Array<String>} */
35+
this.params = [];
36+
}
37+
}
38+
39+
class Router {
40+
constructor() {
41+
/** @type {RouteNode} */
42+
this.root = new RouteNode();
43+
}
44+
45+
/**
46+
* @param {String} path
47+
* @param {HttpMethod} method
48+
* @param {RequestHandler} handler
49+
*/
50+
#verifyParams(path, method, handler) {
51+
if (typeof path !== "string" || path[0] !== "/") throw new Error("Malformed path provided.");
52+
if (typeof handler !== "function") throw new Error("Handler should be a function");
53+
if (!HTTP_METHODS[method]) throw new Error("Invalid HTTP Method");
54+
}
55+
56+
/**
57+
* @param {String} path
58+
* @param {HttpMethod } method
59+
* @param {RequestHandler} handler
60+
*/
61+
#addRoute(path, method, handler) {
62+
this.#verifyParams(path, method, handler);
63+
64+
let currentNode = this.root;
65+
let routeParts = path.split("/").filter(Boolean);
66+
let dynamicParams = [];
67+
68+
for (const segment of routeParts) {
69+
if (segment.includes(" ")) throw new Error("Malformed `path` parameter");
70+
71+
const isDynamic = segment[0] === ":";
72+
const key = isDynamic ? ":" : segment.toLowerCase();
73+
74+
if (isDynamic) {
75+
dynamicParams.push(segment.substring(1));
76+
}
77+
78+
if (!currentNode.children.has(key)) {
79+
currentNode.children.set(key, new RouteNode());
80+
}
81+
82+
currentNode = currentNode.children.get(key);
83+
}
84+
85+
currentNode.handler.set(method, handler);
86+
currentNode.params = dynamicParams;
87+
}
88+
89+
/**
90+
* @param {String} path
91+
* @param {HttpMethod} method
92+
* @returns { { params: Object, handler: RequestHandler } | null }
93+
*/
94+
findRoute(path, method) {
95+
let segments = path.split("/").filter(Boolean);
96+
let currentNode = this.root;
97+
let extractedParams = [];
98+
99+
for (let idx = 0; idx < segments.length; idx++) {
100+
const segment = segments[idx];
101+
102+
let childNode = currentNode.children.get(segment.toLowerCase());
103+
if (childNode) {
104+
currentNode = childNode;
105+
} else if ((childNode = currentNode.children.get(":"))) {
106+
extractedParams.push(segment);
107+
currentNode = childNode;
108+
} else {
109+
return null;
110+
}
111+
}
112+
113+
let params = Object.create(null);
114+
115+
for (let idx = 0; idx < extractedParams.length; idx++) {
116+
let key = currentNode.params[idx];
117+
let value = extractedParams[idx];
118+
119+
params[key] = value;
120+
}
121+
122+
return {
123+
params,
124+
handler: currentNode.handler.get(method),
125+
};
126+
}
127+
128+
/**
129+
* @param {String} path
130+
* @param {RequestHandler} handler
131+
*/
132+
get(path, handler) {
133+
this.#addRoute(path, HTTP_METHODS.GET, handler);
134+
}
135+
136+
/**
137+
* @param {String} path
138+
* @param {RequestHandler} handler
139+
*/
140+
post(path, handler) {
141+
this.#addRoute(path, HTTP_METHODS.POST, handler);
142+
}
143+
144+
/**
145+
* @param {String} path
146+
* @param {RequestHandler} handler
147+
*/
148+
put(path, handler) {
149+
this.#addRoute(path, HTTP_METHODS.PUT, handler);
150+
}
151+
152+
/**
153+
* @param {String} path
154+
* @param {RequestHandler} handler
155+
*/
156+
delete(path, handler) {
157+
this.#addRoute(path, HTTP_METHODS.DELETE, handler);
158+
}
159+
160+
/**
161+
* @param {String} path
162+
* @param {RequestHandler} handler
163+
*/
164+
patch(path, handler) {
165+
this.#addRoute(path, HTTP_METHODS.PATCH, handler);
166+
}
167+
168+
/**
169+
* @param {String} path
170+
* @param {RequestHandler} handler
171+
*/
172+
head(path, handler) {
173+
this.#addRoute(path, HTTP_METHODS.HEAD, handler);
174+
}
175+
176+
/**
177+
* @param {String} path
178+
* @param {RequestHandler} handler
179+
*/
180+
options(path, handler) {
181+
this.#addRoute(path, HTTP_METHODS.OPTIONS, handler);
182+
}
183+
184+
/**
185+
* @param {String} path
186+
* @param {RequestHandler} handler
187+
*/
188+
connect(path, handler) {
189+
this.#addRoute(path, HTTP_METHODS.CONNECT, handler);
190+
}
191+
192+
/**
193+
* @param {String} path
194+
* @param {RequestHandler} handler
195+
*/
196+
trace(path, handler) {
197+
this.#addRoute(path, HTTP_METHODS.TRACE, handler);
198+
}
199+
200+
/**
201+
* @param {RouteNode} node
202+
* @param {number} indentation
203+
*/
204+
printTree(node = this.root, indentation = 0) {
205+
const indent = "-".repeat(indentation);
206+
207+
node.children.forEach((childNode, segment) => {
208+
console.log(`${indent}(${segment}) Dynamic: ${childNode.params}`);
209+
this.printTree(childNode, indentation + 1);
210+
});
211+
}
212+
}
213+
214+
module.exports = Router;
215+
```
216+
217+
#### `lib/constants.js`
218+
219+
```js
220+
const HTTP_METHODS = Object.freeze({
221+
GET: "GET",
222+
POST: "POST",
223+
PUT: "PUT",
224+
DELETE: "DELETE",
225+
PATCH: "PATCH",
226+
HEAD: "HEAD",
227+
OPTIONS: "OPTIONS",
228+
CONNECT: "CONNECT",
229+
TRACE: "TRACE",
230+
});
231+
232+
module.exports = {
233+
HTTP_METHODS,
234+
};
235+
```
236+
237+
#### `lib/index.js`
238+
239+
```js
240+
const { createServer } = require("node:http");
241+
const Router = require("./router");
242+
243+
/**
244+
* Run the server on the specified port
245+
* @param {Router} router - The router to use for routing requests
246+
* @param {number} port - The port to listen on
247+
*/
248+
function run(router, port) {
249+
if (!(router instanceof Router)) {
250+
throw new Error("`router` argument must be an instance of Router");
251+
}
252+
if (typeof port !== "number") {
253+
throw new Error("`port` argument must be a number");
254+
}
255+
256+
createServer(function _create(req, res) {
257+
const route = router.findRoute(req.url, req.method);
258+
259+
if (route?.handler) {
260+
req.params = route.params || {};
261+
route.handler(req, res);
262+
} else {
263+
res.writeHead(404, null, { "content-length": 9 });
264+
res.end("Not Found");
265+
}
266+
}).listen(port);
267+
}
268+
269+
module.exports = { Router, run };
270+
```
271+
272+
#### `globals.js`
273+
274+
```js
275+
/**
276+
* @typedef { 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' | 'CONNECT' | 'TRACE' } HttpMethod
277+
*/
278+
279+
/**
280+
* @typedef {import("http").RequestListener} RequestHandler
281+
*/
282+
```
283+
284+
We've added a new typedef, i.e `RequestHandler` in `globals.js`. This typedef is used to define the type of the handler function that we pass to the `Router` instance. By default, we're using the `RequestListener` type from the `http` module, which is the type of the handler function that the `http.createServer` function expects.
285+
286+
### Your first web server
287+
288+
Now that we've refactored our code, it's time to jump in and put our little project to test. Let's create a simple web server that listens on port `3000` and has a couple of endpoints.
289+
290+
```plaintext
291+
"GET /" -> Hello from the root endpoint
292+
"GET /hello/:name" -> Hello, {name}!
293+
"GET /user/:age/class/:subject" -> You're {age} years old and you're studying {subject}.
294+
```
295+
296+
Here's the code for the web server:
297+
298+
```js
299+
// Get the `Router` and `run` function from our library
300+
const { Router, run } = require("./lib");
301+
302+
// Create a new instance of the `Router` class
303+
const router = new Router();
304+
305+
// Define the routes
306+
router.get("/", (req, res) => {
307+
res.end("Hello from the root endpoint");
308+
});
309+
310+
router.get("/user/:name", (req, res) => {
311+
res.end(`Hello, ${req.params.name}!`);
312+
});
313+
314+
router.get("/user/:age/class/:subject", (req, res) => {
315+
res.end(`You're ${req.params.age} years old, and you're studying ${req.params.subject}.`);
316+
});
317+
318+
// Start the server at port 3000
319+
run(router, 3000);
320+
```
321+
322+
To test our server, we'll make some cURL requests from the terminal.
323+
324+
```plaintext
325+
$ curl http://localhost:3000
326+
Hello from the root endpoint
327+
328+
$ curl http://localhost:3000/user/Ishtmeet
329+
Hello, Ishtmeet!
330+
331+
$ curl http://localhost:3000/user/21/class/Mathematics
332+
You're 21 years old, and you're studying Mathematics.
333+
```
334+
335+
Everything looks good! Our server is up and running, and it's handling the requests as expected.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

src/chapter_06.10/globals.js

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/**
2+
* @typedef { 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' | 'CONNECT' | 'TRACE' } HttpMethod
3+
*/

0 commit comments

Comments
 (0)