Most of the new developers start their Node.js web development journey with the express framework. As you move forward in your career, from one company to another, you have to adopt the framework or tools that your new company is using or your client is pushing you to use something that is new in the community.
It’s very hard to learn everything, maybe not even possible to learn every framework or tool that is available.
Even if you succeed in obtaining knowledge of every available framework, you cannot master them all. In between, some frameworks will become obsolete and some will be succeeded by more advanced.
You may like to ask then what should I do to remain in the competition? you should push yourself to understand the underlying concepts that are the basic building blocks. It will boost your productivity, speed to grab new stuff. You will become more comfortable and familiar with the new stuff/framework easily.
In this series, Just Node.js, No Bullshit!, I m trying to raise the curtain, uncover the concepts behind the web development. Make sure join Bit&Bytes and also join me on Telegram to get latest updates.
In previous post Static Http Routing in Node.js, we created a very basic routing using just Node.js runtime and it's core modules. Now in this post, we are going to create our own minimalist framework from scratch without using any third party library.
Goal
Our framework should be expandable. We can add functionality to the framework using some plugins/middlewares.
Let's start with simple Node.js http server. Create a new file named: server.js.
// server.js
const http = require('http');
async function requestListener(req, res) {
res.end('Request listener called!');
}
http.createServer(requestListener).listen(3000);
Open localhost:3000 in your browser.

Let's code the logic that can call functions one by one. Later we use this logic in our requestListener() function.
Create a new file named 1by1-logic.js and use the below code.
// 1by1-logic.js
const funcs = [
async () => console.log('1'),
async () => console.log('2'),
async () => console.log('3'),
];
const upto = funcs.length - 1;
const fn = async (index) => {
await funcs[index]();
if (index < upto) {
// call only if we have next index
await fn(index + 1);
}
};
fn(0);
fn(0) calls the first function from the array. Compare the index value with length of functions array. if index is less than length, call the function from next index otherwise exit recursion.
Run this: node 1by1-logic.js and you will get 1,2,3 in console.
1
2
3
Let's add support to pass some value to all functions.
// 1by1-logic.js
const funcs = [
async (value) => console.log(`${value} 1`),
async (value) => console.log(`${value} 2`),
async (value) => console.log(`${value} 3`),
];
const upto = funcs.length - 1;
const fn = async (index, value) => {
await funcs[index](value);
if (index < upto) {
// call only if we have next index
await fn(index + 1, value);
}
};
fn(0, 'Hello from');
This time you will get:
Hello from 1
Hello from 2
Hello from 3
Now add this logic to our requestListener() function.
// server.js
const http = require('http');
const frameworkMiddlewares = [
async (req, res) => res.write('I m first\n'),
async (req, res) => res.write('I m second\n'),
async (req, res) => res.end('I m third\n'),
];
async function requestListener(req, res) {
const upto = frameworkMiddlewares.length - 1;
const fn = async (index, req, res) => {
await frameworkMiddlewares[index](req, res);
if (index < upto) {
await fn(index + 1, req, res);
}
};
await fn(0, req, res);
}
http.createServer(requestListener).listen(3000);
Run server.js and open localhost:3000 in your browser.

We are on the path to create our own minimalist framework that can be extended with middlewares. Let's quickly add body parsers. I'm using body parser from a previous post with little modifications.
create new file: body-parser.js
// body-parser.js
function bodyParser(req) {
return new Promise((resolve, reject) => {
// will only allow body in post, put, patch and delete methods
if (['POST', 'PUT', 'PATCH', 'DELETE'].indexOf(req.method) < 0) {
resolve();
return;
}
let data = '';
req.on('data', (chunk) => {
data += chunk.toString();
})
req.on('end', () => {
try {
// @ts-ignore
req.body = JSON.parse(data);
} catch (e) {
// @ts-ignore
req.body = {};
}
resolve();
});
req.on('error', (e) => {
reject(e);
});
});
}
module.exports = bodyParser;
File server.js changes
// server.js
const http = require('http');
const frameworkMiddlewares = [];
function use(fn) {
frameworkMiddlewares.push(fn);
}
async function requestListener(req, res) {
const upto = frameworkMiddlewares.length - 1;
const fn = async (index, req, res) => {
await frameworkMiddlewares[index](req, res);
if (index < upto) {
await fn(index + 1, req, res);
}
};
await fn(0, req, res);
}
function listen(...args) {
const server = http.createServer(requestListener);
return server.listen(...args)
}
module.exports = {
use,
listen,
};
Instead of staring server inline, we created a function with name listen, that will start our server. Function use() created to add middlewares to our server. We can extend our framework using this use() function.
Let's attach a body parser.
Create a new file named: app.js and import the body parser and our server into it.
// app.js
const app = require('./server');
const bodyParser = require('./body-parser');
app.use(bodyParser);
// Request Controller
app.use(async (req, res) => {
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify(req.body));
});
app.listen(3000);
Now instead of server.js, run app.js file and in place of browser use postman. Post some json to localhost:3000.

Note: Header application/json is important to properly parse json on client side.
Comment this line and try again
// res.writeHead(200, { 'content-type': 'application/json' });

Note: Ordering of .use() is important because we are calling our framework middlewares one by one. So if you move the bodyParser below the request controller, output may be blank. Try changing the ordering of app.use() calls to see the difference.
Now let's bring in our router.js file from a previous post Static Http Routing in Node.js and code a middleware function that will plug that router into our framework.
// router.js
const router = {
// this is our missing route handler
'*': (req, res) => {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: 'Not Found'
}));
},
};
function register(httpCommand, httpHandler) {
if (router[httpCommand]) {
throw new Error(`Command ${httpCommand} already exists.`);
}
router[httpCommand] = httpHandler;
}
function findHandler(req) {
const handler = router[req.method + req.url] || router['*'];
return handler;
}
function middleware() {
return async (req, res) => {
const handler = findHandler(req);
await handler(req, res);
}
}
module.exports = {
findHandler,
register,
middleware,
};
Now change app.js file to plug this router into our framework. Also define some routes for testing.
// app.js
const app = require('./server');
const bodyParser = require('./body-parser');
const router = require('./router');
const reqTime = require('./req-time');
router.register('GET/', (req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
message: 'Welcome to my custom router!',
}));
});
router.register('POST/product', (req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
product: req.body,
}));
});
app.use(bodyParser);
app.use(router.middleware());
app.listen(3000);
Note: Router middleware is a function that returns the original middleware function, so we had to call it. bodyParser is a ref to function, no need to call it. It upon you what kind of middleware you want.
Open postman and try the defined routes.

Our framework should be expandable, Goal achieved! We plugged the body parser and router into it. Let's add one more middleware and wrap up this post.
Create new file: req-time.js
// req-time.js
// millisecond in 1 nano second
const MS_PER_NS = 1e-6;
function before() {
return (req) => {
req.reqStartAtNsecs = process.hrtime.bigint();
};
}
function after() {
return (req, res) => {
const reqCompletedAtNsecs = process.hrtime.bigint();
const ms = Number(reqCompletedAtNsecs - req.reqStartAtNsecs) * MS_PER_NS;
console.log(`${req.method} ${req.url} ${res.statusCode} ${ms.toFixed(3)}ms`);
};
}
module.exports = {
before,
after,
};
This plugin will track time taken by requests in milliseconds. process.hrtime.bigint() returns high-resolution real time in nanoseconds as a bigint. So to convert it to milliseconds, we first need to convert it to real number.
You may ask, Why not Date.now(). As this method returns milliseconds. Try Date.now() yourself and let me know in the comments, Why not Date.now().
More on process.hrtime.bigint()
Let's plug this into our framework. Open app.js and modify it.
// app.js
const app = require('./server');
const bodyParser = require('./body-parser');
const router = require('./router');
const reqTime = require('./req-time');
router.register('GET/', (req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
message: 'Welcome to my custom router!',
}));
});
router.register('POST/product', (req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
product: req.body,
}));
});
app.use(reqTime.before());
app.use(bodyParser);
app.use(router.middleware());
app.use(reqTime.after());
app.listen(3000);
Note: Again ordering of middlewares matters.
Congratulations, our minimalist framework is ready. We will improve this further in future posts.
Please feel free to ask questions, clear your doubts or share feedback in the comments section. Let me know in the comments, what other concepts or topics you would like me to cover in this series of posts.
For latest updates: join me on Telegram.
Comments (0)
Login or create an account to leave a comment.
No comments yet. Be the first!