litepath
Version:
Microframework ligero para Node.js
208 lines (186 loc) • 7.01 kB
JavaScript
// litepath.js
// Microframework minimalista similar a Express, hecho en Node.js
// Permite manejar rutas, middlewares, errores y archivos estáticos
const http = require('http'); // Servidor HTTP nativo de Node
const { URL } = require('url'); // Para parsear URLs
const fs = require('fs'); // Para manejar archivos
const path = require('path'); // Para manejo de rutas de archivos
// Tipos MIME para archivos estáticos
const MIME_TYPES = {
'.html': 'text/html; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.js': 'application/javascript; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.gif': 'image/gif'
};
class LitePath {
constructor() {
this.routes = []; // Array de rutas registradas
this.middlewares = []; // Middlewares normales
this.errorMiddlewares = []; // Middlewares de manejo de errores
this.staticFolder = null; // Carpeta de archivos estáticos
}
// ------------------------
// Convierte rutas con parámetros "/user/:id" en RegExp y keys
// ------------------------
pathToRegex(pathDef) {
const keys = [];
const pattern = pathDef.replace(/\/:([^/]+)/g, (_, k) => {
keys.push(k); // Guarda los nombres de los parámetros
return '/([^/]+)'; // Convierte en regex
});
return { regex: new RegExp('^' + pattern + '$'), keys };
}
// ------------------------
// Métodos HTTP para registrar rutas
// ------------------------
get(path, handler) { this.addRoute('GET', path, handler); }
post(path, handler) { this.addRoute('POST', path, handler); }
put(path, handler) { this.addRoute('PUT', path, handler); }
delete(path, handler) { this.addRoute('DELETE', path, handler); }
// ------------------------
// Registra una ruta en el microframework
// ------------------------
addRoute(method, pathDef, handler) {
const { regex, keys } = this.pathToRegex(pathDef);
this.routes.push({ method, pathDef, regex, keys, handler });
}
// ------------------------
// Registra middlewares
// Si tiene 4 parámetros, se considera middleware de error
// ------------------------
use(fn) {
if (fn.length === 4) this.errorMiddlewares.push(fn);
else this.middlewares.push(fn);
}
// ------------------------
// Configura carpeta para archivos estáticos
// ------------------------
serveStatic(folder) {
this.staticFolder = path.resolve(folder);
}
// ------------------------
// Inicia el servidor en un puerto específico
// ------------------------
listen(port, cb) {
const server = http.createServer((req, res) => this.handle(req, res));
server.listen(port, cb);
}
// ------------------------
// Manejo de errores con middlewares de error
// ------------------------
handleError(err, req, res) {
if (this.errorMiddlewares.length) {
let idx = 0;
const next = (e) => {
const fn = this.errorMiddlewares[idx++];
if (!fn) {
res.statusCode = 500;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ error: e.message || 'Error interno' }));
return;
}
try { fn(e, req, res, next); }
catch (err2) { next(err2); }
};
next(err);
} else {
res.statusCode = 500;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ error: err.message || 'Error interno' }));
}
}
// ------------------------
// Función principal que maneja todas las peticiones
// ------------------------
async handle(req, res) {
// Parseo de URL y query params
const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
req.path = url.pathname;
req.query = Object.fromEntries(url.searchParams.entries());
// ------------------------
// Métodos de respuesta: send y json
// ------------------------
res.send = (statusOrBody, maybeBody) => {
let status = 200, body = statusOrBody;
if (typeof statusOrBody === 'number') {
status = statusOrBody;
body = maybeBody;
}
if (typeof body === 'object') {
res.setHeader('Content-Type', 'application/json');
body = JSON.stringify(body);
}
res.statusCode = status;
res.end(body);
};
res.json = (obj) => {
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(obj));
};
// ------------------------
// Parseo de body para POST, PUT y PATCH
// ------------------------
req.body = null;
if (['POST','PUT','PATCH'].includes(req.method)) {
const chunks = [];
await new Promise(resolve => {
req.on('data', c => chunks.push(c));
req.on('end', resolve);
});
const raw = Buffer.concat(chunks).toString();
const ct = (req.headers['content-type'] || '');
if (ct.includes('application/json')) {
try { req.body = JSON.parse(raw || '{}'); } catch { req.body = {}; }
} else if (ct.includes('application/x-www-form-urlencoded')) {
req.body = Object.fromEntries(new URLSearchParams(raw));
} else {
req.body = raw;
}
}
// ------------------------
// Servir archivos estáticos
// ------------------------
if (this.staticFolder) {
let filePath = path.join(this.staticFolder, req.path === '/' ? 'index.html' : req.path);
try {
const stat = fs.statSync(filePath);
if (stat.isFile()) {
const ext = path.extname(filePath);
res.writeHead(200, { 'Content-Type': MIME_TYPES[ext] || 'application/octet-stream' });
fs.createReadStream(filePath).pipe(res);
return;
}
} catch {}
}
// ------------------------
// Manejo de rutas dinámicas
// ------------------------
const route = this.routes.find(r => r.method === req.method && r.regex.test(req.path));
if (route) {
const m = req.path.match(route.regex);
req.params = {};
route.keys.forEach((k, i) => { req.params[k] = decodeURIComponent(m[i+1]); });
const handlers = [...this.middlewares, route.handler];
let idx = 0;
const next = (err) => {
if (err) return this.handleError(err, req, res);
const fn = handlers[idx++];
if (!fn) return;
try {
const r = fn(req, res, next);
if (r && r.then) r.catch(next);
} catch (e) { next(e); }
};
return next();
}
// ------------------------
// Ruta no encontrada
// ------------------------
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('404 - Not Found');
}
}
module.exports = LitePath;