Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
April 2, 2023 08:25 pm GMT

Creating your own ExpressJS from scratch (Part 1) - Basics, Methods, and Routing

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-frameworkcd web-frameworknpm 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

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

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

    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

    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

    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

    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

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

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!


Original Link: https://dev.to/wesleymreng7/creating-your-own-expressjs-from-scratch-part-1-basics-methods-and-routing-a8

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To