Skip to content

Instantly share code, notes, and snippets.

@herudi
Last active January 13, 2021 07:14
Show Gist options
  • Save herudi/126fd895aef994b3a42fc66d42bc45ab to your computer and use it in GitHub Desktop.
Save herudi/126fd895aef994b3a42fc66d42bc45ab to your computer and use it in GitHub Desktop.
Native nodejs http server like express
const http = require('http');
const pathnode = require('path');
const { parse: parseqs } = require('querystring');
const { parse: parsenodeurl } = require('url');
const METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS', 'ALL'];
const PRE_METHOD = 'GET,POST';
const PUSH = Array.prototype.push;
const TYPE = 'Content-Type';
const JSON_TYPE = 'application/json';
const JSON_CHARSET = JSON_TYPE + ';charset=utf-8';
function addRes(res, eng) {
res.set = function (name, value) {
this.setHeader(name, value);
return this;
};
res.get = function (name) {
return this.getHeader(name);
};
res.code = function (code) {
this.statusCode = code;
return this;
};
res.status = function (code) {
this.statusCode = code;
return this;
};
res.type = function (type) {
this.setHeader(TYPE, type);
return this;
};
res.json = function (data) {
data = JSON.stringify(data);
this.setHeader(TYPE, JSON_CHARSET);
this.end(data);
};
res.send = function (data) {
if (typeof data === 'string') this.end(data);
else if (typeof data === 'object') this.json(data);
else this.end(data || http.STATUS_CODES[this.statusCode]);
};
res.render = function (src, ...args) {
let idx = src.indexOf('.'),
obj = eng[Object.keys(eng)[0]],
pathfile = pathnode.join(obj.basedir, src + obj.ext);
if (idx !== -1) {
obj = eng[src.substring(idx)];
pathfile = pathnode.join(obj.basedir, src);
}
return obj.render(res, pathfile, ...args);
};
}
function defError(err, req, res, next) {
let code = err.status || err.code || err.statusCode || 500;
if (typeof code !== 'number') code = 500;
res.statusCode = code;
res.end(err.message || 'Something went wrong');
}
function defNotFound(message) {
return function (req, res, next) {
res.statusCode = 404;
res.end(`Route ${message} not found`)
}
}
function findBase(pathname) {
let iof = pathname.indexOf('/', 1);
if (iof !== -1) return pathname.substring(0, iof);
return pathname;
}
function parseurl(req) {
let str = req.url, url = req._parsedUrl;
if (url && url._raw === str) return url;
return (req._parsedUrl = parsenodeurl(str));
}
function findFn(arr) {
let ret = [], i = 0, len = arr.length;
for (; i < len; i++) {
if (typeof arr[i] === 'function') ret.push(arr[i]);
}
return ret;
}
function pathRegex(path) {
if (path instanceof RegExp) return { params: null, regex: path };
let pattern = path.replace(/\/:[a-z]+/gi, '/([^/]+?)');
let regex = new RegExp(`^${pattern}/?$`, 'i');
let matches = path.match(/\:([a-z]+)/gi);
let params = matches && matches.map(e => e.substring(1));
return { params, regex };
}
function mutateRoute(route, c_route) {
METHODS.forEach(el => {
if (c_route[el] !== void 0) {
if (route[el] === void 0) route[el] = [];
route[el] = route[el].concat(c_route[el]);
};
});
return route;
}
function patchRoutes(arg, args, routes) {
let prefix = '', midds = [], i = 0, len = routes.length, ret = {};
midds = midds.concat(findFn(args));
if (typeof arg === 'string' && arg.length > 1 && arg.charAt(0) === '/') prefix = arg;
for (; i < len; i++) {
let el = routes[i];
let { params, regex } = pathRegex(prefix + el.path);
el.handlers = midds.concat(el.handlers);
if (ret[el.method] === void 0) ret[el.method] = [];
ret[el.method].push({ params, regex, handlers: el.handlers });
}
return ret;
};
function renderEngine(obj) {
return function (res, source, ...args) {
if (obj.options) args.push(obj.options);
if (!args.length) args.push({ settings: obj.settings });
if (typeof obj.engine === 'function') {
obj.engine(source, ...args, (err, out) => {
if (err) throw err;
res.setHeader(TYPE, 'text/html; charset=utf-8');
res.end(out);
});
}
}
}
class Router {
constructor() {
this.route = { 'MIDDS': [] };
this.c_routes = [];
this.get = this.on.bind(this, 'GET');
this.post = this.on.bind(this, 'POST');
this.put = this.on.bind(this, 'PUT');
this.patch = this.on.bind(this, 'PATCH');
this.delete = this.on.bind(this, 'DELETE');
this.options = this.on.bind(this, 'OPTIONS');
this.head = this.on.bind(this, 'HEAD');
this.all = this.on.bind(this, 'ALL');
}
on(method, path, ...handlers) {
this.c_routes.push({ method, path, handlers });
return this;
}
getRoute(method, path, notFound) {
if (this.route['ALL'] !== void 0) {
if (this.route[method] === void 0) this.route[method] = [];
this.route[method] = this.route[method].concat(this.route['ALL']);
}
let i = 0, j = 0, el, routes = this.route[method] || [], matches = [], params = {}, handlers = [], len = routes.length, nf;
while (i < len) {
el = routes[i];
if (el.regex.test(path)) {
nf = false;
if (el.params) {
matches = el.regex.exec(path);
while (j < el.params.length) params[el.params[j]] = matches[++j] || null;
}
PUSH.apply(handlers, el.handlers);
break;
}
i++;
}
if (notFound) handlers.push(notFound);
else handlers.push(defNotFound(method + path));
return { params, handlers, nf };
}
};
class Tinex extends Router {
constructor({ useServer, useParseUrl } = {}) {
super();
this.server = useServer;
this.parseurl = useParseUrl || parseurl;
this.mroute = {};
this.error = defError;
this.notFound = undefined;
this.engine = {};
}
onError(fn) {
this.error = fn;
}
onNotFound(fn) {
this.notFound = fn;
}
on(method, path, ...handlers) {
let { params, regex } = pathRegex(path);
if (this.route[method] === void 0) this.route[method] = [];
this.route[method].push({ params, regex, handlers });
return this;
}
use(...args) {
let arg = args[0], larg = args[args.length - 1], len = args.length;
if (len === 1 && typeof arg === 'function') this.route['MIDDS'].push(arg);
else if (typeof arg === 'string' && typeof larg === 'function') {
let prefix = arg === '/' ? '' : arg, fns = [];
fns.push((req, res, next) => {
req.url = req.url.substring(prefix.length) || '/';
req.path = req.path ? req.path.substring(prefix.length) || '/' : '/';
next();
});
fns = fns.concat(findFn(args));
this.route[prefix] = fns;
}
else if (typeof larg === 'object' && larg.c_routes) mutateRoute(this.route, patchRoutes(arg, args, larg.c_routes));
else if (typeof larg === 'object' && larg.engine) {
let defaultDir = pathnode.join(pathnode.dirname(require.main.filename || process.mainModule.filename), 'views')
let ext = arg.ext,
basedir = pathnode.resolve(arg.basedir || defaultDir),
render = arg.render;
if (render === void 0) {
let engine = (typeof arg.engine === 'string' ? require(arg.engine) : arg.engine);
if (typeof engine === 'object' && engine.renderFile !== void 0) engine = engine.renderFile;
ext = ext || ('.' + (typeof arg.engine === 'string' ? arg.engine : 'html'));
render = renderEngine({ engine, options: arg.options, settings: { views: basedir, ...(arg.set ? arg.set : {}) } })
}
this.engine[ext] = { ext, basedir, render };
}
else if (Array.isArray(larg)) {
let el, i = 0, len = larg.length;
for (; i < len; i++) {
el = larg[i];
if (typeof el === 'object' && el.c_routes) mutateRoute(this.route, patchRoutes(arg, args, el.c_routes));
else if (typeof el === 'function') this.route['MIDDS'].push(el);
};
}
else this.route['MIDDS'] = this.route['MIDDS'].concat(findFn(args));
return this;
}
lookup(req, res) {
let cnt = 0; for (let k in this.mroute) cnt++;
if (cnt > 32) this.mroute = {};
let url = this.parseurl(req),
key = req.method + url.pathname,
obj = this.mroute[key], i = 0,
next = (err = null) => {
if (err) return this.error(err, req, res, next);
obj.handlers[i++](req, res, next);
};
if (obj === void 0) {
obj = this.getRoute(req.method, url.pathname, this.notFound);
if (PRE_METHOD.indexOf(req.method) !== -1 && obj.nf !== void 0) this.mroute[key] = obj;
}
addRes(res, this.engine);
req.originalUrl = req.url;
req.params = obj.params;
req.path = url.pathname;
req.query = parseqs(url.query);
req.search = url.search;
if (obj.m === void 0) {
let prefix = findBase(url.pathname), midds = this.route['MIDDS'];
if (this.route[prefix] !== void 0) obj.handlers = this.route[prefix].concat(obj.handlers);
obj.handlers = midds.concat(obj.handlers);
obj.m = true;
if (PRE_METHOD.indexOf(req.method) !== -1 && obj.nf !== void 0) this.mroute[key] = obj;
};
next();
}
listen(port = 3000, ...args) {
const server = this.server || http.createServer();
server.on('request', this.lookup.bind(this));
server.listen(port, ...args);
}
};
let tinex = ({ useServer, useParseUrl } = {}) => new Tinex({ useServer, useParseUrl });
tinex.router = () => new Router();
tinex.Router = Router;
module.exports = tinex;
@herudi
Copy link
Author

herudi commented Jan 13, 2021

Usage

Simple

require('./tinex')().all('/', (req, res) => res.send('Hello')).listen(3000);

Middleware

const tinex = require('./tinex');
function midd1(req, res, next){
    req.foo = 'foo';
    next();
}
function midd2(req, res, next){
    req.bar = 'bar';
    next();
}
const app = tinex();
app.use(midd1);
app.get('/foobar', midd2, (req, res) => {
    res.send(`${req.foo} ${req.bar}`);
});
app.listen(3000, () => {
    console.log('Running on ' + 3000);
});

Router

const tinex = require('./tinex');
const app = tinex();
const router = tinex.router();
const router2 = tinex.router();
router.get('/hello', (req, res) => {
    res.send('router');
});
router2.get('/hello2', (req, res) => {
    res.send('router2');
});
app.use('/api', [router, router2]);
app.listen(3000, () => {
    console.log('Running on ' + 3000);
});

Body Parser

yarn add body-parser

const tinex = require('./tinex');
const bodyParser = require('body-parser');
const app = tinex();

// parse application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({ extended: false }))
 
// parse application/json
app.use(bodyParser.json())

app.post('/body', (req, res) => {
    res.send(req.body);
})
app.listen(3000, () => {
    console.log('Running on ' + 3000);
});

Template Engine

yarn add ejs
default folder in views and create hello.ejs for example.

const tinex = require('./tinex');
const app = tinex();
app.use({engine: 'ejs'});
app.get('/hello', (req, res) => {
    res.render('hello', {
        name: 'Herudi'
    });
});
app.listen(3000, () => {
    console.log('Running on ' + 3000);
});

Serve Static

yarn add serve-static

const tinex = require('./tinex');
const serveStatic = require('serve-static');
const app = tinex();
app.use('/assets', serveStatic('public'));
app.listen(3000, () => {
    console.log('Running on ' + 3000);
});

Error Handling

const tinex = require('./tinex');
const app = tinex();
app.get('/hello', (req, res, next) => {
    try {
        req.missMethod();
        res.send('hello');
    } catch(err) {
        next(err);
    }
})
app.onError((err, req, res, next) => {
    res.send(err.message);
})
app.onNotFound((req, res, next) => {
    res.code(404).send('Url Not Found');
})
app.listen(3000, () => {
    console.log('Running on ' + 3000);
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment