nutraj
Version:
A lightweight Express‑like framework with multi‑domain support, middleware, dynamic routing, static file serving, and built‑in utilities such as cookie parsing, logging, CORS, sessions, and optional API statistics.
397 lines (363 loc) • 11.6 kB
JavaScript
// nutraj.js
const http = require('http');
const fs = require('fs');
const path = require('path');
const querystring = require('querystring');
const url = require('url');
const formidable = require('formidable'); // For multipart/form-data parsing
function Nutraj() {
const routes = [];
const middlewares = [];
const domains = {};
// In-memory session store.
const sessionStore = {};
function generateSessionId() {
return Math.random().toString(36).substring(2) + Date.now().toString(36);
}
// Helper to compile parameterized routes like '/user/:id'
function compilePath(routePath) {
const keys = [];
const regexString = routePath
.replace(/([.+*?=^!:${}()[\]|\/\\])/g, '\\$1')
.replace(/\\:(\w+)/g, (match, key) => {
keys.push(key);
return '([^/]+)';
});
const regex = new RegExp('^' + regexString + '$');
return { regex, keys };
}
// --- Built-in Middlewares ---
// Cookie parser: populates req.cookies with key/value pairs.
function cookieParser(req, res, next) {
req.cookies = {};
const cookieHeader = req.headers.cookie;
if (cookieHeader) {
cookieHeader.split(';').forEach(cookie => {
const parts = cookie.split('=');
req.cookies[parts[0].trim()] = decodeURIComponent(parts[1] || '');
});
}
next();
}
// Logger: logs request method, URL, status code and duration.
function logger(req, res, next) {
const start = Date.now();
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
res.on('finish', () => {
const duration = Date.now() - start;
console.log(
`[${new Date().toISOString()}] ${req.method} ${req.url} ${res.statusCode} - ${duration}ms`
);
});
next();
}
// CORS: sets Access-Control headers.
function cors(options = {}) {
return function (req, res, next) {
res.setHeader('Access-Control-Allow-Origin', options.origin || '*');
res.setHeader(
'Access-Control-Allow-Methods',
options.methods || 'GET,HEAD,PUT,PATCH,POST,DELETE'
);
res.setHeader(
'Access-Control-Allow-Headers',
options.headers || 'Origin, X-Requested-With, Content-Type, Accept, Authorization'
);
if (req.method === 'OPTIONS') {
res.writeHead(204);
return res.end();
}
next();
};
}
/**
* Body parser middleware:
* - Parses JSON if Content-Type includes "application/json"
* - Parses URL-encoded data if Content-Type includes "application/x-www-form-urlencoded"
* - Uses formidable to parse multipart/form-data (for file uploads)
*/
function bodyParser(req, res, next) {
const contentType = req.headers['content-type'] || '';
const method = req.method.toLowerCase();
if (['post', 'put', 'patch'].includes(method)) {
if (contentType.includes('multipart/form-data')) {
const form = formidable({ multiples: true });
form.parse(req, (err, fields, files) => {
if (err) {
next(err);
return;
}
req.body = fields;
req.files = files;
next();
});
return;
} else {
let data = [];
req.on('data', chunk => data.push(chunk));
req.on('end', () => {
const rawData = Buffer.concat(data).toString();
if (contentType.includes('application/json')) {
try {
req.body = JSON.parse(rawData);
} catch (e) {
req.body = {};
}
} else if (contentType.includes('application/x-www-form-urlencoded')) {
req.body = querystring.parse(rawData);
}
next();
});
return;
}
}
next();
}
/**
* Session middleware:
* - Looks for a cookie "nutraj_session"; if missing, creates one.
* - Attaches a session object to req.session and saves it in memory.
*/
function sessionMiddleware(req, res, next) {
const cookies = req.cookies || {};
let sessionId = cookies['nutraj_session'];
if (!sessionId) {
sessionId = generateSessionId();
res.setHeader('Set-Cookie', `nutraj_session=${sessionId}; HttpOnly`);
}
req.session = sessionStore[sessionId] || {};
res.on('finish', () => {
sessionStore[sessionId] = req.session;
});
next();
}
/**
* Static file serving middleware generator.
* Use as: app.use(app.static('public'))
*/
function staticMiddleware(staticDir) {
return function (req, res, next) {
const filePath = path.join(staticDir, req.url);
fs.stat(filePath, (err, stats) => {
if (err || !stats.isFile()) {
return next();
}
const stream = fs.createReadStream(filePath);
stream.on('error', next);
stream.pipe(res);
});
};
}
/**
* User-Agent parsing middleware.
* Attaches the user-agent string from the request headers to req.useragent.
*/
function userAgentMiddleware(req, res, next) {
req.useragent = req.headers['user-agent'] || '';
next();
}
/**
* API Statistics middleware:
* When used, it sets a flag on the request (req.enableStats = true)
* so that the matched route will record its statistics.
*/
function statsMiddleware(req, res, next) {
req.enableStats = true;
next();
}
/**
* Enhance the response object with helper methods:
* - res.send(): sends a string or JSON response.
* - res.json(): sends a JSON response.
* - res.sendFile(): streams a file to the client.
* - res.redirect(): redirects the client to a URL.
*/
function enhanceResponse(req, res) {
res.send = function (body) {
if (typeof body === 'object') {
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(body));
} else {
res.setHeader('Content-Type', 'text/html');
res.end(String(body));
}
};
res.json = function (data) {
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(data));
};
res.sendFile = function (filePath) {
fs.stat(filePath, (err, stats) => {
if (err || !stats.isFile()) {
res.statusCode = 404;
res.end('File not found');
} else {
const stream = fs.createReadStream(filePath);
stream.on('error', () => {
res.statusCode = 500;
res.end('Error reading file');
});
stream.pipe(res);
}
});
};
res.redirect = function (redirectUrl, statusCode) {
statusCode = statusCode || 302;
res.statusCode = statusCode;
res.setHeader('Location', redirectUrl);
res.end();
};
}
/**
* The main application function.
* - Enhances req and res objects.
* - Parses the URL to extract query parameters and pathname.
* - Checks for multi-domain delegation.
* - Processes middleware and routes.
* - Uses default error handling if needed.
*/
function app(req, res) {
enhanceResponse(req, res);
// Parse URL to populate req.query and req.path.
const parsedUrl = url.parse(req.url, true);
req.query = parsedUrl.query;
req.path = parsedUrl.pathname;
// Multi-domain handling: delegate if a sub‑app is registered.
let host = req.headers.host;
if (host) {
host = host.split(':')[0];
if (domains[host]) {
return domains[host](req, res);
}
}
let idx = 0;
function next(err) {
if (idx < middlewares.length) {
const middleware = middlewares[idx++];
if (err) {
if (middleware.length === 4) {
return middleware(err, req, res, next);
}
return next(err);
} else {
if (middleware.length < 4) {
return middleware(req, res, next);
}
return next();
}
} else {
if (err) {
res.statusCode = 500;
res.end('Internal Server Error: ' + err.message);
} else {
handleRoutes();
}
}
}
function handleRoutes() {
const method = req.method.toLowerCase();
for (const route of routes) {
if (route.method === method || route.method === 'all') {
if (route.regex) {
const match = route.regex.exec(req.path);
if (match) {
req.params = {};
route.keys.forEach((key, index) => {
req.params[key] = match[index + 1];
});
recordAndHandle(route);
return;
}
} else if (route.path === req.path) {
req.params = {};
recordAndHandle(route);
return;
}
}
}
res.statusCode = 404;
res.end('Not Found');
}
// If API statistics are enabled for this request (req.enableStats),
// record the start time and update stats when the response finishes.
function recordAndHandle(route) {
if (req.enableStats) {
const startTime = Date.now();
res.on('finish', () => {
const elapsed = Date.now() - startTime;
route.stats.hits++;
route.stats.totalTime += elapsed;
route.stats.averageTime = route.stats.totalTime / route.stats.hits;
});
}
route.handler(req, res);
}
next();
}
// --- API for Middleware and Routing ---
// Register middleware. Use either a path prefix or a function.
app.use = function (pathOrMiddleware, maybeMiddleware) {
if (typeof pathOrMiddleware === 'string') {
const pathPrefix = pathOrMiddleware;
const middleware = maybeMiddleware;
middlewares.push((req, res, next) => {
if (req.path.startsWith(pathPrefix)) {
return middleware(req, res, next);
}
next();
});
} else {
middlewares.push(pathOrMiddleware);
}
};
// Routing methods with dynamic parameter support.
['get', 'post', 'put', 'delete', 'patch', 'all'].forEach(method => {
app[method] = function (routePath, handler) {
const route = {
method,
path: routePath,
handler,
stats: { hits: 0, totalTime: 0, averageTime: 0 }
};
if (routePath.includes(':')) {
const { regex, keys } = compilePath(routePath);
route.regex = regex;
route.keys = keys;
}
routes.push(route);
};
});
// Register sub‑apps for specific domains.
app.domain = function (domain, subApp) {
domains[domain] = subApp;
};
// --- Expose built‑in middlewares ---
app.cookieParser = cookieParser;
app.logger = logger;
app.cors = cors;
app.bodyParser = bodyParser;
app.session = sessionMiddleware;
app.static = staticMiddleware;
app.useragent = userAgentMiddleware;
// API statistics middleware – add with app.use(app.stats) to enable stats tracking.
app.stats = statsMiddleware;
// --- Built-in API Statistics Endpoint ---
// Access at GET /__stats to view stats for each registered route.
app.get('/__stats', (req, res) => {
const stats = routes.map(route => ({
method: route.method,
path: route.path,
hits: route.stats.hits,
averageResponseTime: route.stats.averageTime
}));
res.json({ routes: stats });
});
// Start the HTTP server.
app.listen = function (port, callback) {
const server = http.createServer(app);
server.listen(port, callback);
return server;
};
return app;
}
module.exports = Nutraj;