Creating your own ExpressJS from scratch (Part 1) - Basics, Methods, and Routing
Tags: webdev, node, javascript, tutorial
Published: 2023-04-02 20:25:00

Hey, have you ever thought about creating your web framework using NodeJS, to understand what is happening under the hood, the methods, the middlewares, controllers, and so on...

I know we have a lot of great frameworks to create awesome applications, like ExpressJS, NestJS, Fastify, etc. But you know what are they doing, I think the majority of the developers even don't care about it, only want to create their applications, and that's ok, but might they have a better experience creating applications and understanding what the framework is doing, allowing extends the framework's functionalities, fix some unexpected errors, improve performance issues, in general, make their life easier.

Along the way to creating our Web Framework, We are going to work with several concepts related to Javascript, NodeJS, and programming in general, like design patterns and regular expressions.

**Application**: Today we are going to create the first part of our Web Framework, to make us able to create routes and handle these routes.

**Requirements**:
- NodeJS `16.16` version

---

## Setup

We are going to create a basic setup here, only to be able to continue working.


```
mkdir web-framework

cd web-framework

npm init -y
```

- Open the terminal you prefer
- Create a folder with the name that you want.
- Enter in the folder
- Start a new npm project

---

## Imports

`src/app.js`

```js
const { createServer } = require('http')
const { match } = require('path-to-regexp')
```
The first line imports the `createServer` function from Node.js's built-in `http` module, which allows us to create an HTTP server. The second line imports the match function from the `path-to-regexp` package, which allows us to match URLs to specific routes.

To install `path-to-regex` package:

``` 
npm install path-to-regex
```

---


## Application

`src/app.js`

```js
const App = () => {
    const routes = new Map()
    const createMyServer = () => createServer(serverHandler.bind(this))
```

The `App` function is the main entry point for our web application framework. It creates a new `Map` object called routes to store all our defined routes. It also defines a helper function called `createMyServer` that creates an HTTP server using the `serverHandler` function, a function that We will create soon, will be the main handler.

---


## Route Definition

It is not my intention to cover all the HTTP methods, but only show some of them to serve as an example.

`src/app.js`

```js
    const get = (path, ...handlers) => {
        const currentHandlers = routes.get(`${path}/GET`) || []
        routes.set(`${path}/GET`, [...currentHandlers, ...handlers])
    }

    const post = (path, ...handlers) => {
        const currentHandlers = routes.get(`${path}/POST`) || []
        routes.set(`${path}/POST`, [...currentHandlers, ...handlers])
    }

    const put = (path, ...handlers) => {
        const currentHandlers = routes.get(`${path}/PUT`) || []
        routes.set(`${path}/PUT`, [...currentHandlers, ...handlers])
    }

    const patch = (path, ...handlers) => {
        const currentHandlers = routes.get(`${path}/PATCH`) || []
        routes.set(`${path}/PATCH`, [...currentHandlers, ...handlers])
    }

    const del = (path, ...handlers) => {
        const currentHandlers = routes.get(`${path}/DELETE`) || []
        routes.set(`${path}/DELETE`, [...currentHandlers, ...handlers])
    }
```
These are the route definition functions, which define a new route and specify the handlers for each HTTP method (GET, POST, PUT, PATCH, DELETE). They take a `path` argument and any number of `handlers`. Each function retrieves the current handlers for the specified method and path from the `routes` object, adds any new handlers, and sets the updated handlers back into the `routes` object.

the pattern to match with the route and find the right handler must contain the route and at the end the name of the method. like the examples below:

For `/test/1/GET` Path is `/test/1` and the method is `GET`

For `/test1/2/test2/POST` Path is `/test1/2/test2` and the method is `POST`

The `Map` is a good way to store the handlers because here we can use some useful functions like `get` and `keys`.

---

## Treating the URLs

For our routing system works well We need to treat the URLs, because they don't come as we expect.

`src/app.js`

```js
    const sanitizeUrl = (url, method) => {
        // Get the last part of the URL, removing the domain
        const urlParams = url.split('/').slice(1)

        // Remove querystrings from the last parameter
        const [lastParam] = urlParams[urlParams.length - 1].split('?')
        urlParams.splice(urlParams.length - 1, 1)

        // Create the URL with our pattern
        const allParams = [...urlParams, lastParam].join('/')
        const sanitizedUrl = `/${allParams}/${method.toUpperCase()}`

        return sanitizedUrl
    }

    const matchUrl = (sanitizedUrl) => {
        for (const path of routes.keys()) {
            const urlMatch = match(path, {
                decode: decodeURIComponent,
            })

            const found = urlMatch(sanitizedUrl)

            if (found) {
                return path
            }
        }
        return false
    }
```

These are helper functions used to sanitize and match URLs to their corresponding routes. `sanitizeUrl` takes a URL and an HTTP method and removes any query strings, then concatenates the parameters into a pattern matching the structure of the route. `matchUrl` iterates through the routes checking if the URL matches with one of our current routes.


---

## Handling the Server

`src/app.js`

```js
    const serverHandler = async (request, response) => {
        const sanitizedUrl = sanitizeUrl(request.url, request.method)

        const match = matchUrl(sanitizedUrl)

        if (match) {
            const middlewaresAndControllers = routes.get(match)
            console.log(middlewaresAndControllers)
            response.statusCode = 200
            response.end('Found')
        } else {
            response.statusCode = 404
            response.end('Not found')
        }
    }
```

The function first calls `sanitizeUrl()` with the `request.url` and `request.method` as arguments to create a sanitized URL, which is used to check against the routes set up in the framework.

The function then calls `matchUrl()` with the sanitized URL to try and find a matching route set up in the framework. If a match is found, it retrieves the list of middlewares and controllers associated with that route from the routes Map.

If there are middlewares and/or controllers associated with the route, the function logs them to the console and sets the response status code to 200, indicating that the request was successful. Finally, it ends the response with the message `'Found'`.

If a match is not found, the function sets the response status code to 404, indicating that the requested resource was not found. Finally, it ends the response with the message `'Not found'`.

---


## The Run function and Exports

`src/app.js`

```js
    const run = (port) => {
        const server = createMyServer()
        server.listen(port)
    }


    return {
        run,
        get,
        post,
        patch,
        put,
        del
    }
```

The `run` function is responsible for starting the server by calling the listen method of the server instance. The `listen` method accepts a port number as an argument, which is the port on which the server will listen for incoming requests.

In this function, a new instance of the server is created by calling the `createMyServer` function, which is defined earlier in the code. Then, the `listen` method is called on the server instance, passing in the port argument provided to the `run` function.

Finally, we can export all the public functions as a `closure` returning them.

---


## Entire code

`src/app.js`

```js
const { createServer } = require('http')
const { match } = require('path-to-regexp')

const App = () => {
    const routes = new Map()
    const createMyServer = () => createServer(serverHandler.bind(this))


    const get = (path, ...handlers) => {
        const currentHandlers = routes.get(`${path}/GET`) || []
        routes.set(`${path}/GET`, [...currentHandlers, ...handlers])
    }

    const post = (path, ...handlers) => {
        const currentHandlers = routes.get(`${path}/POST`) || []
        routes.set(`${path}/POST`, [...currentHandlers, ...handlers])
    }

    const put = (path, ...handlers) => {
        const currentHandlers = routes.get(`${path}/PUT`) || []
        routes.set(`${path}/PUT`, [...currentHandlers, ...handlers])
    }

    const patch = (path, ...handlers) => {
        const currentHandlers = routes.get(`${path}/PATCH`) || []
        routes.set(`${path}/PATCH`, [...currentHandlers, ...handlers])
    }

    const del = (path, ...handlers) => {
        const currentHandlers = routes.get(`${path}/DELETE`) || []
        routes.set(`${path}/DELETE`, [...currentHandlers, ...handlers])
    }

    const sanitizeUrl = (url, method) => {
        const urlParams = url.split('/').slice(1)

        // remove querystrings from the last parameter
        const [lastParam] = urlParams[urlParams.length - 1].split('?')
        urlParams.splice(urlParams.length - 1, 1)

        // create the URL with our pattern
        const allParams = [...urlParams, lastParam].join('/')
        const sanitizedUrl = `/${allParams}/${method.toUpperCase()}`

        return sanitizedUrl
    }

    const matchUrl = (sanitizedUrl) => {
        for (const path of routes.keys()) {
            const urlMatch = match(path, {
                decode: decodeURIComponent,
            })

            const found = urlMatch(sanitizedUrl)

            if (found) {
                return path
            }
        }
        return false
    }


    const serverHandler = async (request, response) => {
        const sanitizedUrl = sanitizeUrl(request.url, request.method)

        const match = matchUrl(sanitizedUrl)

        if (match) {
            const middlewaresAndControllers = routes.get(match)
            console.log(middlewaresAndControllers)
            response.statusCode = 200
            response.end('Found')
        } else {
            response.statusCode = 404
            response.end('Not found')
        }
    }

    const run = (port) => {
        const server = createMyServer()
        server.listen(port)
    }


    return {
        run,
        get,
        post,
        patch,
        put,
        del
    }
}

module.exports = App
```

---


## Using and Testing

I created a separate file called `index.js` outside the `src` folder to test our framework.

`index.js`

```js
const App = require('./src/app')

const app = App()


app.get('/test/test2', function test() { }, function test2() { })
app.post('/test', (req, res) => console.log('test'))
app.patch('/test', (req, res) => console.log('test'))
app.put('/test', (req, res) => console.log('test'))
app.del('/test', (req, res) => console.log('test'))



const start = async () => {
    app.run(3000)
}

start()
```

You can execute this file and test the application using your favorite HTTP client.

```
node index.js
```


---

## Next Steps

In the next tutorial, we are working with `middlewares` and `controllers`. If you liked this tutorial don't hesitate to give your reaction and follow me to see at the first hand my next tutorials. I am trying to write at least one tutorial per week.

Thanks, see you soon!