Skip to content

Commit

Permalink
adds chapter 06.11 - Buildling a web server
Browse files Browse the repository at this point in the history
  • Loading branch information
ishtms committed Aug 29, 2024
1 parent 41b1f62 commit 8238a34
Show file tree
Hide file tree
Showing 20 changed files with 825 additions and 2 deletions.
5 changes: 4 additions & 1 deletion Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ The repo for our backend framework- [Velocy](https://github.com/ishtms/velocy).
- [`for..of`](chapters/ch03-working-with-files.md#-for-of-)
- [`for await..of`](chapters/ch03-working-with-files.md#-for-await-of-)
- [Reading the `json` file](chapters/ch03-working-with-files.md#reading-the-json-file)
- [Buffers](chapters/ch03-working-with-files.md#buffers)
- [Buffers](chapters/ch03-working-with-files.md#buffers)
- [Parsing the `json` file](chapters/ch03-working-with-files.md#parsing-the-json-file)
- [`logtar` our own logging library](chapters/ch04-logtar-our-logging-library.md#-logtar-our-own-logging-library)
- [Initializing a new project](chapters/ch04-logtar-our-logging-library.md#initializing-a-new-project)
Expand Down Expand Up @@ -266,4 +266,7 @@ The repo for our backend framework- [Velocy](https://github.com/ishtms/velocy).
- [Refactoring the `TrieRouter` class](chapters/ch06.10-running-our-server.md#refactoring-the-trierouter-class)
- [Type Aliases](chapters/ch06.10-running-our-server.md#type-aliases)
- [The `run` function](chapters/ch06.10-running-our-server.md#the-run-function)
- [Building our first web-server](chapters/ch06.11-building-a-web-server.md#building-our-first-web-server)
- [More refactoring](chapters/ch06.11-building-a-web-server.md#more-refactoring)
- [Your first web server](chapters/ch06.11-building-a-web-server.md#your-first-web-server)

2 changes: 1 addition & 1 deletion chapters/ch03-working-with-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -849,7 +849,7 @@ Strangely, this outputs some weird looking stuff

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.

# Buffers
## Buffers

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

Expand Down
8 changes: 8 additions & 0 deletions chapters/ch06.10-running-our-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ function run(router, port) {
const route = router.findRoute(req.url, req.path);

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

if (route?.handler) {
req.params = route.params;
route.handler(req, res);
} else {
res.writeHead(404, null, { "Content-Length": 9 });
Expand All @@ -204,8 +206,14 @@ if (route?.handler) {
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.
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`.
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-)
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.
> \*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.
```

```
335 changes: 335 additions & 0 deletions chapters/ch06.11-building-a-web-server.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,335 @@
## Building our first web-server

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.

### More refactoring

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.

Here's the updated file structure:

```plaintext
 ./
├──  lib/ # Our library code
│ ├──  constants.js # Constants used in our library
│ ├──  index.js # Entry point of our library
│ └──  router.js # Router implementation
├──  globals.js # Global Typedefs
└──  test.js # We'll write our code for testing here.
```

#### `lib/router.js`

```js
const { HTTP_METHODS } = require("./constants");

class RouteNode {
constructor() {
/** @type {Map<String, RouteNode>} */
this.children = new Map();

/** @type {Map<String, RequestHandler>} */
this.handler = new Map();

/** @type {Array<String>} */
this.params = [];
}
}

class Router {
constructor() {
/** @type {RouteNode} */
this.root = new RouteNode();
}

/**
* @param {String} path
* @param {HttpMethod} method
* @param {RequestHandler} handler
*/
#verifyParams(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");
}

/**
* @param {String} path
* @param {HttpMethod } method
* @param {RequestHandler} handler
*/
#addRoute(path, method, handler) {
this.#verifyParams(path, method, handler);

let currentNode = this.root;
let routeParts = path.split("/").filter(Boolean);
let dynamicParams = [];

for (const segment of routeParts) {
if (segment.includes(" ")) throw new Error("Malformed `path` parameter");

const isDynamic = segment[0] === ":";
const key = isDynamic ? ":" : segment.toLowerCase();

if (isDynamic) {
dynamicParams.push(segment.substring(1));
}

if (!currentNode.children.has(key)) {
currentNode.children.set(key, new RouteNode());
}

currentNode = currentNode.children.get(key);
}

currentNode.handler.set(method, handler);
currentNode.params = dynamicParams;
}

/**
* @param {String} path
* @param {HttpMethod} method
* @returns { { params: Object, handler: RequestHandler } | null }
*/
findRoute(path, method) {
let segments = path.split("/").filter(Boolean);
let currentNode = this.root;
let extractedParams = [];

for (let idx = 0; idx < segments.length; idx++) {
const segment = segments[idx];

let childNode = currentNode.children.get(segment.toLowerCase());
if (childNode) {
currentNode = childNode;
} else if ((childNode = currentNode.children.get(":"))) {
extractedParams.push(segment);
currentNode = childNode;
} else {
return null;
}
}

let params = Object.create(null);

for (let idx = 0; idx < extractedParams.length; idx++) {
let key = currentNode.params[idx];
let value = extractedParams[idx];

params[key] = value;
}

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

/**
* @param {String} path
* @param {RequestHandler} handler
*/
get(path, handler) {
this.#addRoute(path, HTTP_METHODS.GET, handler);
}

/**
* @param {String} path
* @param {RequestHandler} handler
*/
post(path, handler) {
this.#addRoute(path, HTTP_METHODS.POST, handler);
}

/**
* @param {String} path
* @param {RequestHandler} handler
*/
put(path, handler) {
this.#addRoute(path, HTTP_METHODS.PUT, handler);
}

/**
* @param {String} path
* @param {RequestHandler} handler
*/
delete(path, handler) {
this.#addRoute(path, HTTP_METHODS.DELETE, handler);
}

/**
* @param {String} path
* @param {RequestHandler} handler
*/
patch(path, handler) {
this.#addRoute(path, HTTP_METHODS.PATCH, handler);
}

/**
* @param {String} path
* @param {RequestHandler} handler
*/
head(path, handler) {
this.#addRoute(path, HTTP_METHODS.HEAD, handler);
}

/**
* @param {String} path
* @param {RequestHandler} handler
*/
options(path, handler) {
this.#addRoute(path, HTTP_METHODS.OPTIONS, handler);
}

/**
* @param {String} path
* @param {RequestHandler} handler
*/
connect(path, handler) {
this.#addRoute(path, HTTP_METHODS.CONNECT, handler);
}

/**
* @param {String} path
* @param {RequestHandler} handler
*/
trace(path, handler) {
this.#addRoute(path, HTTP_METHODS.TRACE, handler);
}

/**
* @param {RouteNode} node
* @param {number} indentation
*/
printTree(node = this.root, indentation = 0) {
const indent = "-".repeat(indentation);

node.children.forEach((childNode, segment) => {
console.log(`${indent}(${segment}) Dynamic: ${childNode.params}`);
this.printTree(childNode, indentation + 1);
});
}
}

module.exports = Router;
```

#### `lib/constants.js`

```js
const HTTP_METHODS = Object.freeze({
GET: "GET",
POST: "POST",
PUT: "PUT",
DELETE: "DELETE",
PATCH: "PATCH",
HEAD: "HEAD",
OPTIONS: "OPTIONS",
CONNECT: "CONNECT",
TRACE: "TRACE",
});

module.exports = {
HTTP_METHODS,
};
```

#### `lib/index.js`

```js
const { createServer } = require("node:http");
const Router = require("./router");

/**
* Run the server on the specified port
* @param {Router} router - The router to use for routing requests
* @param {number} port - The port to listen on
*/
function run(router, port) {
if (!(router instanceof Router)) {
throw new Error("`router` argument must be an instance of Router");
}
if (typeof port !== "number") {
throw new Error("`port` argument must be a number");
}

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

if (route?.handler) {
req.params = route.params || {};
route.handler(req, res);
} else {
res.writeHead(404, null, { "content-length": 9 });
res.end("Not Found");
}
}).listen(port);
}

module.exports = { Router, run };
```
#### `globals.js`
```js
/**
* @typedef { 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' | 'CONNECT' | 'TRACE' } HttpMethod
*/

/**
* @typedef {import("http").RequestListener} RequestHandler
*/
```
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.
### Your first web server
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.
```plaintext
"GET /" -> Hello from the root endpoint
"GET /hello/:name" -> Hello, {name}!
"GET /user/:age/class/:subject" -> You're {age} years old and you're studying {subject}.
```
Here's the code for the web server:
```js
// Get the `Router` and `run` function from our library
const { Router, run } = require("./lib");

// Create a new instance of the `Router` class
const router = new Router();

// Define the routes
router.get("/", (req, res) => {
res.end("Hello from the root endpoint");
});

router.get("/user/:name", (req, res) => {
res.end(`Hello, ${req.params.name}!`);
});

router.get("/user/:age/class/:subject", (req, res) => {
res.end(`You're ${req.params.age} years old, and you're studying ${req.params.subject}.`);
});

// Start the server at port 3000
run(router, 3000);
```
To test our server, we'll make some cURL requests from the terminal.
```plaintext
$ curl http://localhost:3000
Hello from the root endpoint

$ curl http://localhost:3000/user/Ishtmeet
Hello, Ishtmeet!

$ curl http://localhost:3000/user/21/class/Mathematics
You're 21 years old, and you're studying Mathematics.
```
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.
3 changes: 3 additions & 0 deletions src/chapter_06.10/globals.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/**
* @typedef { 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' | 'CONNECT' | 'TRACE' } HttpMethod
*/
Loading

0 comments on commit 8238a34

Please sign in to comment.