[![Read Prev](/assets/imgs/prev.png)](/chapters/ch06.01-basic-router-implementation.md)

## The `Router` class

<!--Master Node.js by building a backend framework with zero dependencies. Learn about Router class, HTTP server integration, and efficient memory management.-->

To begin with, let's create a basic version of the `Router` class for better understanding. We can gradually add more functionality to it as we move forward. The first implementation of the `Router` class will allow us to add routes and handle basic HTTP requests.

```js
// file: index.js

class Router {
  constructor() {
    // Stores our routes
    this.routes = {};
  }
}
```

The `routes` member variable in our `Router` class serves as an internal data structure for managing the mapping between URL paths and corresponding handlers. This approach provides the benefits of encapsulation and efficient route lookups. However, this approach is not suitable for dynamic routes such as `/api/:version` or `/api/account/:account_id/transactions`, where there are infinite possible URLs that can match. This issue will be addressed later. For now, we will stick to static routes such as `/api/users` or `/api/account/signup`.

Let's introduce a new helper method called `addRoute` in the `Router` class. This method will help us bind callback functions to execute whenever a request for a particular endpoint is made.

```js
// file: index.js

class Router {
  constructor() {
    this.routes = {};
  }

  addRoute(method, path, handler) {
    this.routes[`${method} ${path}`] = handler;
  }
}
```

The first argument, `method` is the HTTP method - `GET`, `POST`, `PUT` etc. The second argument is the URL or the path of the request, and the third argument is the callback function that will be called for that particular `method` and `path` combination. Let's see an example on how will be it called.

```js
// file: index.js

class Router { ... }

const router = new Router();

router.addRoute('GET', '/', () => console.log('Hello from GET /'));
```

Let us add a method inside our `Router` class which will print all the routes, for debugging purposes.

```js
// file: index.js

class Router {
    constructor() { ... }

    addRoute(method, path, handler) { ... }

    printRoutes() {
        console.log(Object.entries(this.routes));
    }
}

const router = new Router();
router.addRoute('GET', '/', () => console.log('Hello from GET /'));
router.addRoute('POST', '/', () => console.log('Hello from POST /'));

router.printRoutes()
```

Now, let us give it a try:

```js
$ node index.js
// Outputs
[
  [ 'GET /', [Function (anonymous)] ],
  [ 'POST /', [Function (anonymous)] ]
]
```

The `Object.entries` method converts an object, with key-value pairs, into a tuple array with only two elements. The first element is the key, and the second element is the corresponding value.

The `function (anonymous)` signature isn't helpful at all. Let's change it:

```js
// file: index.js

...

const router = new Router();
router.addRoute('GET', '/', function handleGetBasePath() { console.log(...) });
router.addRoute('POST', '/',function handlePostBasePath() { console.log(...) });

router.printRoutes()
```

Outputs:

```js
[
  [ 'GET /', [Function: handleGetBasePath] ],
  [ 'POST /', [Function: handlePostBasePath] ]
]
```

Much better. I generally tend to avoid anonymous functions as much as I can, and give them a proper name instead. The above can also be written as:

```js
// file: index.js

function handleGetBasePath() { ... }
function handlePostBasePath() { ... }

router.addRoute("GET", "/", handleGetBasePath)
router.addRoute("POST", "/", handlePostBasePath)
```

Let us try to hook our router with a real HTTP server. We'll have to import the `node:http` module, as well as add an utility method inside the `Router` class that redirects the incoming request to their appropriate handlers.

### Using `Router` with an HTTP server

```js
// file: index.js

class Router {
    ...
    handleRequest(request, response) {
        const { url, method } = request;
        this.routes[`${method} ${url}`](request, response);
    }
}
```

Since we only care about the `url` and `method` of an HTTP request, we're destructuring it out of the `request` object. There's a weird looking syntax, that might be foreign to you if you're new to javascript.

```js
this.routes[`${method} ${url}`](request, response);
```

The above is a short way to write:

```js
let functionToExecute = this.routes[`${method} ${url}`];
functionToExecute(request, response);
```

You need to be careful with this syntax though. If `path` and `method` combination isn't registered, it will return `undefined`. And the above syntax would resolve to `undefined()`, that in fact does not make any sense. Javascript will throw a beautiful error message -

```bash
undefined is not a function
```

To take care of it, let's add a simple check:

```js
class Router {
    ...
    handleRequest(request, response) {
        const { url, method } = request;
        const handler = this.routes[`${method} ${url}`];

        if (!handler) {
            return console.log('404 Not found')
        }

        handler(request, response)
    }
}
```

Now let's forward every request to this method.

```js
// file: index.js

const http = require('node:http')
const PORT = 5255;

class Router { ... }

const router = new Router();
router.addRoute('GET', '/', function handleGetBasePath() { console.log(...) });
router.addRoute('POST', '/', function handlePostBasePath() { console.log(...) });

let server = http.createServer(function serveRequest(request, response) {
    router.handleRequest(request, response)
})

server.listen(PORT)
```

We've imported the `node:http` module, to create an HTTP server, and used the `http.createServer` method, providing it a callback that takes 2 arguments - first one is the request, and the second one is the response object.

We can still make our code a little better. Instead of passing a callback that has only one job, i.e. calls another method; we can directly pass the target method as an argument:

```js
// file: index.js

class Router {...}

/** __ Add routes __ **/

let server = http.createServer(router.handleRequest);
server.listen(PORT)
```

Or even shorter, in case you do not wish to access any methods of the `http.Server` object returned by `http.createServer`.

```js
http.createServer(router.handleRequest).listen(PORT);
```

Let us test it using `cURL`, after starting the server using `node index` on a different terminal:

```bash
$ curl http://localhost:5255 -v
*   Trying 127.0.0.1:5255...
* Connected to localhost (127.0.0.1) port 5255 (#0)
> GET / HTTP/1.1
> Host: localhost:5255
> User-Agent: curl/7.87.0
> Accept: */*
>
* Empty reply from server
* Closing connection 0
curl: (52) Empty reply from server
```

An empty reply from server? Let's head over to the node program's console. Oops, a crash!

```js
TypeError: Cannot read properties of undefined (reading 'GET /')
        at Server.handleRequest (/Users/ishtmeet/Code/velocy/index.js:16:36)
```

It's saying we tried to access the key `GET /` of an undefined value. It's pointing at this line:

```js
const handler = this.routes[`${method} ${url}`];
```

It is saying that `this.routes` is undefined. Weird, isn't it? No. If you have written some code with javascript previously, you would've already figured out the issue. The culprit is this line:

```js
http.createServer(router.handleRequest);
```

Let us try to print what the value of `this` is, in the `handleRequest` method:

```js
class Router {
    ...
    handleRequest(request, response) {
        console.log(this.constructor.name);
        ...
    }
}
```

Send another `cURL` request:

```bash
# Prints
Server
```

How?

## `this` is not good

Let's take a moment to understand a huge part of Javascript programming in general: the `this` keyword. We won't go deep into the nitty gritty of `this` but we'll understand enough to not fall into this weird semantic bug in our programs.

When we tried to log `this.constructor.name`, it printed `Server` which has nothing to do with our code. We don't have a class or function named `Server` in our code. It means that the `this` context inside `handleRequest` is an instance of Node's native HTTP Server class, not our `Router` class.

The reason is how `this` works in JavaScript when passing a method as a callback. When we originally had:

```js
// we're passing the router.handleRequest method here as an argument
let server = http.createServer(router.handleRequest);

// The `handleRequest` method is defined as a normal function, not an arrow function
class Router {
    handleRequest(request, response) {
        this.routes; // Looks good but not good.
        ...
    }
}
```

The `this` value in the `handleRequest` method will not be the original object (`router` in this case), but will be determined by how it's called — which, in the case of `http.createServer`, won't be the `router` object. That's why `this.routes` is `undefined`.

It turns out that there is nothing wrong with the method definition inside the `Router` class. However, there is an issue with the way it is being invoked.

The method `handleRequest` is passed as a callback to `http.createServer()`. When this callback gets invoked by the Node.js HTTP Server, the context (`this`) inside `handleRequest` is bound to that Server instance, not to the `Router` instance.

We're passing a reference to the `handleRequest` method, but it loses its context (`this`), i.e. it gets dissociated from the `router` instance of the `Router` class. When the `handleRequest` method is invoked by the HTTP server, this is set to the HTTP Server object, not the `router` instance.

How do we fix this? There are two ways: an old way and a modern one. Let's see the old way first:

#### Using `.bind()`

```js
let server = http.createServer(router.handleRequest.bind(router));
```

The `.bind` method returns a new function that is a "bound" version of the `handleRequest` method, such that the `this` context within that method is set to the `router` instance.

So, `.bind()` ensures that when `handleRequest` is called, the value of `this` inside of it will be our `router` object. Before `ES6` or `EcmaScript 2015`, this was the standard way of solving issues with the **this** keyword.

Let's take a look at a more convenient way, i.e use an `Arrow` function:

#### Using `Arrow` function

```js
let server = http.createServer((req, res) => router.handleRequest(req, res));

// or if you prefer named functions
const handleRequest = (req, res) => router.handleRequest(req, res);
const server = http.createServer(handleRequest);
```

Unlike normal functions, arrow functions don't have their own `this`. Instead, they inherit the `this` value from the surrounding lexical context where they were defined. This lexical scoping for `this` is one of the most useful features of arrow functions.

I'll explain what I mean by the **lexical** context.

### Lexical Context

**Lexical context** (or lexical scope) is the area where a certain variable is accessible or has meaning. When a variable is defined, it's confined to a particular scope, and it can't be accessed from outside of that scope.

```bash
Global Scope
|
|-- const global = "I'm global";
|
|-- function outerFunction() {
|     |
|     |-- const outer = "I'm in the outer function";
|     |
|     |-- function innerFunction() {
|     |     |
|     |     |-- const inner = "I'm in the inner function";
|     |     |
|     |     |-- // Can access inner, outer, and global
|     |}
|     |
|     |-- // Can access outer and global, but NOT inner
|}
|
|-- // Can access global, but NOT outer or inner
```

Let's look at another example using classes:

```js
class Person {
  constructor() {
    this.name = "Ishtmeet";
  }

  regularFunction() {
    setTimeout(function () {
      // `this` here is not the Person instance, it's either the window object
      // in browsers or `global` in Node.js
      console.log(this.name); // Undefined or error
    }, 1000);
  }

  arrowFunction() {
    setTimeout(() => {
      // `this` here is lexically bound, it's the Person instance
      console.log(this.name); // Outputs: "Ishtmeet"
    }, 1000);
  }
}
```

One more example using classes:

```js
class Player {
  constructor() {
    this.health = 52;
  }

  regularFunction() {
    // Will output 52 if called as an instance method
    console.log(this.health);
  }

  arrowFunction = () => {
    // Will also output 52 if called as an instance method
    console.log(this.health);
  };
}
```

For example:

```js
const mainCharacter = new Player();

mainCharacter.regularFunction(); // Outputs 52
mainCharacter.arrowFunction(); // Outputs 52
```

But, let's see what happens if we extract these methods and call them independently of the class instance:

```js
const extractedRegularFn = mainCharacter.regularFunction;
const extractedArrowFn = mainCharacter.arrowFunction;

extractedRegularFn(); // Outputs undefined or throws an error
extractedArrowFn(); // Outputs 52
```

In the code above, `extractedRegularFn()` outputs `undefined` or throws a TypeError depending on strict mode because it loses its original `this` context.

On the other hand, `extractedArrowFn()` still outputs `52`, because the arrow function doesn't have its own `this`; it uses the `this` from the lexical scope where it was defined (inside the `Player` constructor, because it is bound to `mainCharacter` as we specifically called it on `mainCharacter` using `mainCharacter.arrowFunction`).

### Arrow functions are not free

Keep in mind, arrow functions come with a slight performance penalty. It is usually negligible for most applications, but can be heavy on memory if we're creating a lot of objects of a certain class. This won't be an issue with our `Router`, but it's worth knowing this.

When arrow functions are defined as class methods/properties, a new function object is created for each instance of the class, rather than each time the function is invoked.

For example, let's look at this example:

```js
class Monster {
  regularMethod() { ...  }
  arrowMethod = () => { ... };
}

const boss = new Monster();
const creep = new Monster()
```

In this case, both `boss` and `creep` will have their own copy of `arrowMethod`, because it's defined as an instance property using arrow syntax. Each time a new `Monster` object is created, new memory is allocated for `arrowMethod`.

On the other hand, `regularMethod` is defined on `Monster.prototype`, meaning that all instances of `Monster` share the same `regularMethod` function object. This is generally more memory-efficient.

In a game, one can spawn thousands, if not millions of monsters. Or imagine another hypothetical example of a photo editing application, that stores every pixel on the screen as an object of a `Pixel` class. There are going to be millions of pixels on the screen, and every extra allocation for the function body may be slight overwhelming for the memory constraints.

### Why should we care about memory?

We are focusing on building a high-performance backend framework, it is important to consider the impact of memory allocations. While the `Router` class may only have 10-15 instances, we may introduce our custom `Response` or `Request` class in the future. If we create every function as an arrow function, our framework will allocate unnecessary memory for applications receiving high load, such as those receiving thousands of requests per second. An easy way to picture arrow functions in mind is as follows:

```js
class Response {
  constructor() {
    // If these arrow functions are created new for every instance of Response,
    // and the site is currently in a heavy load situation, receiving 5k requests per second
    // we'd be creating 5,000 * num_of_arrow_functions new function instances per second
    this.someMethod = () => {
      /*... */
    };
    this.anotherMethod = () => {
      /* ... */
    };
    // ... more arrow functions
  }
}
```

Creating separate function objects for each instance is usually not a concern for most applications. However, in cases where you have a very large number of instances or if instances are frequently created and destroyed, this could lead to increased memory usage and garbage collection activity.

It's also worth noting that if we're building a library or a base class that other developers will extend, using prototype methods (regular methods) allows for easier method overriding and usage of the `super` keyword.

> Note: Unless we're in a very performance-critical scenario or creating a vast number of instances, the difference is likely negligible. Most of the time, the decision between using arrow functions or regular methods in classes comes down to semantics and specific requirements around `this` binding.

### Testing the updated code

Our code in the `index.js` file should look something like this:

```js
// file: index.js

const http = require("node:http");

const PORT = 5255;

class Router {
  constructor() {
    this.routes = {};
  }

  addRoute(method, path, handler) {
    this.routes[`${method} ${path}`] = handler;
  }

  handleRequest(request, response) {
    const { url, method } = request;
    const handler = this.routes[`${method} ${url}`];

    if (!handler) {
      return console.log("404 Not found");
    }

    handler(request, response);
  }

  printRoutes() {
    console.log(Object.entries(this.routes));
  }
}

const router = new Router();
router.addRoute("GET", "/", function handleGetBasePath() {
  console.log("Hello from GET /");
});

router.addRoute("POST", "/", function handlePostBasePath() {
  console.log("Hello from POST /");
});

// Note: We're using an arrow function instead of a regular function now
let server = http.createServer((req, res) => router.handleRequest(req, res));
server.listen(PORT);
```

Let us try to execute this code, and send a request through `cURL`:

We see the output `Hello from GET /` on the server console. But, there's still something wrong on the client (`cURL`):

```bash
$ curl http://localhost:5255/ -v
*   Trying 127.0.0.1:5255...
* Connected to localhost (127.0.0.1) port 5255 (#0)
> GET / HTTP/1.1
> Host: localhost:5255
> User-Agent: curl/7.87.0
> Accept: */*
>
```

The request headers are shown, means the request was made successfully, although server did not send any response back. Why is it so?

We're observing this behavior because the server has not indicated to the client (in this case, `cURL`) that the request has been fully processed and the response has been completely sent. Well, how do we indicate that?

We do that using `.end()` method on the response object. But how can we get access to that inside our callback functions `handlePostBasePath()` and `handleGetBasePath()`? Turns out, they're already supplied to these functions when we did this:

```js
// pass the `request` as the first argument, and `response` as the second.
let server = http.createServer((req, res) => router.handleRequest(req, res));
```

The `http.createServer` method requires a callback function, and provides request object as the first argument, and the response object as the second.

On updating the code:

```js
// file: index.js

...

router.addRoute("GET", "/", function handleGetBasePath(req, res) {
    console.log("Hello from GET /");
    res.end();
});

router.addRoute("POST", "/", function handlePostBasePath(req, res) {
    console.log("Hello from POST /");
    res.end()
});

...
```

Now if you try to make a request to any endpoint, the server will respond back with appropriate response body:

```bash
$ curl http://localhost:5255/ -v
*   Trying 127.0.0.1:5255...
* Connected to localhost (127.0.0.1) port 5255 (#0)
→ GET / HTTP/1.1
→ Host: localhost:5255
→ User-Agent: curl/7.87.0
→ Accept: */*
→
* Mark bundle as not supporting multiuse
← HTTP/1.1 200 OK
← Date: Thu, 07 Sep 2023 13:04:39 GMT
← Connection: keep-alive
← Keep-Alive: timeout=5
← Content-Length: 0
←
* Connection #0 to host localhost left intact
```

We also set up a 404 handler, in case a route is not configured. We're also going to add a `response.end()` to indicate the client that the request has been processed.

```js
class Router {
    ...
    handleRequest(request, response) {
        ...
        if (!handler) {
            console.log("404 Not found");
            response.writeHead(404, { 'Content-Type': 'text/plain' })
            return response.end('Not found');
        }
        ...
    }
}
```

Let's check whether it returns `404`, if the route is not registered?

```bash
$ curl http://localhost:5255/not/found -v
*   Trying 127.0.0.1:5255...
* Connected to localhost (127.0.0.1) port 5255 (#0)
→ GET /not/found HTTP/1.1
... request body trimmed ...
→
* Mark bundle as not supporting multiuse
← HTTP/1.1 404 Not Found
← Content-Type: text/plain
... response body trimmed ...
* Connection #0 to host localhost left intact
Not found
```

Great! In the next section, we will explore how to make our router API even more elegant by eliminating the need to specify the name of the HTTP method every time we define a new endpoint handler. So, instead of writing:

```js
router.addRoute("POST", "/", function handlePostBasePath(req, res) {
  console.log("Hello from POST /");
  res.end();
});
```

We could do something like:

```js
router.get("/", function handlePostBasePath(req, res) {
    console.log("Hello from POST /");
    res.end();
});
```

This way, we'll provide a clean and clear interface for our clients.

[![Read Next](/assets/imgs/next.png)](/chapters/ch06.03-improving-the-router-api.md)
