UNPKG

litepath

Version:

Microframework ligero para Node.js

208 lines (186 loc) 7.01 kB
// 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;