Creating your own ExpressJS from scratch (Part 1) - Basics, Methods, and Routing
Wesley Miranda
Posted on April 2, 2023
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
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!
Posted on April 2, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
April 9, 2023
April 2, 2023