## Building our first web-server

<!-- Learn Node.js by building a backend framework from scratch. Understand how to create a simple web server using the `http` module and our custom `Router` class. -->

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.
