@jysperu/load-express
v2.0.9
Published
Configura Express con CORS, compresión, minificación, timeout, cookie-parser, Pug y utilidades para APIs REST con respuestas estandarizadas
Maintainers
Readme
@jysperu/load-express
Paquete para inicializar Express con middlewares listos para producción, helpers para APIs/Pug e internacionalización con i18n.
Instalación
npm install @jysperu/load-expressInicio rápido
import { init } from "@jysperu/load-express";
await init({
addRoutes(app) {
app.get("/api/v1/ping", (req, res) => {
res.success("pong");
});
},
});API pública
Export principal
default->initExpressinit(options?)init(addRoutesFn)initExpress-> alias deinitclose()closeExpress-> alias decloseExpressApp()-> función que retorna la instancia Express actualExpressServerInstance()-> función que retorna la instancia del servidor HTTPport(force?)url(force?)addPugContextCallback(callback)
Aliases exportados
ExpressJsonResult-> alias dejsonResultinternoAntiAbuseReasons— objeto con los mensajes de bloqueo deantiAbuse
Estado/objetos exportados
ExpressApp()(función getter que retorna la instancia Express actual onull)ExpressServerInstance()(función getter que retorna la instancia del servidor HTTP onull)express(librería Express)Router(Router de Express)
Tipos exportados (principales)
InitOptions,callableAddRoutesExpress,Request,Response,NextFunction,Middleware,ExpressServerHelpersOptions,HelpersDefaults,CallablePugContext,EnvValsSecureHeadersOptions,SecureHeadersDefaultsRateLimitOptions,RateLimitDefaultsAntiAbuseOptions,AntiAbuseDefaultsTimeOutTime,TimeOutTimeValue,TimeOutTimeCallback,TimeoutOptionsCorsOptions,JsonOptions,UrlEncodedOptionsCookieParserOptions,CookieParserSecret,CookieParserOptionsParameterSanitizeHtmlOptions,SanitizeHtmlTargetCompressionOptionsMinifyOptions,MinifyCacheOption,MinifyCacheCallbackI18nOptions,UrlDataResultHookPayload,ResultHook
Funciones exportadas (avanzado)
Todas las funciones de middleware están disponibles individualmente:
import {
setHelpers,
secureHeaders,
rateLimit,
antiAbuse,
timeout,
cors,
json,
urlencoded,
cookieParser,
sanitizeHtml,
compression,
minify,
minifyCacheOption,
i18n,
parseUrl,
publicJS,
getClientIp,
getClientUserAgent,
getPath,
getHost,
getReferer,
defaultKeyGenerator,
helpersDefaults,
secureHeadersDefaults,
rateLimitDefaults,
antiAbuseDefaults,
timeoutDefaults,
envVals,
ExpressApp,
ExpressServerInstance,
} from "@jysperu/load-express";Variables de entorno
PORToEXPRESS_PORT: puerto del servidor (default3000)URLoEXPRESS_URL: URL base (defaulthttp://localhost:{PORT})EXPRESS_APIPATH: regex de rutas API (default^/api(?:/v[0-9]+(\.[0-9]+)?)?/)EXPRESS_AUTHPATH: regex de rutas auth (default^(?:/api(?:/v[0-9]+(\.[0-9]+)?)?)?/auth/)EXPRESS_PUGPATH: carpeta de vistas Pug (default./pug/pages)
InitOptions
init acepta dos formas:
init(options?)— async, devuelvePromise<Express | false>init(addRoutesFn)— async, devuelvePromise<Express | false>
InitOptions (forma init(options)) acepta:
addRoutes?: (app) => voidaddPugContextCallback?: CallablePugContextprerequests?: Middleware[]— middlewares adicionales que se registran trassetHelpersapiPattern?: string | RegExpauthPattern?: string | RegExppugPath?: stringsecureHeaders?: SecureHeadersOptions | booleanrateLimit?: RateLimitOptions | booleanantiAbuse?: AntiAbuseOptions | booleantimeout?: TimeOutTime | TimeoutOptions | booleancors?: CorsOptions | booleani18n?: I18nOptionsjson?: JsonOptions | booleanurlencoded?: UrlEncodedOptions | booleancookieParser?: CookieParserOptions | booleansanitizeHtml?: SanitizeHtmlOptions | booleancompression?: CompressionOptions | booleanminify?: MinifyOptions | booleani18nPublicRoute?: string | false(default"/i18n.js")onResultSuccess?: ResultHook— se ejecuta cuandoresult.success === trueonResultFailure?: ResultHook— se ejecuta cuandoresult.success === falseonResultRedirect?: ResultHook— se ejecuta cuando el HTTP code es 301 o 302onResultComplete?: ResultHook— se ejecuta siempre al finalizar unresult()oredirectResult()
Todos los middlewares (salvo i18n y setHelpers) pueden desactivarse con false.
Middlewares incluidos en init
Orden real de ejecución en init():
cookieParser(si no esfalse)json(si no esfalse)urlencoded(si no esfalse)i18nsetHelpersprerequests(si se proporcionan)secureHeaders(si no esfalse)rateLimit(si no esfalse)antiAbuse(si no esfalse)sanitizeHtml(si no esfalse)timeout(si no esfalse)cors(si no esfalse)compression(si no esfalse)minify(si no esfalse)- Ruta
i18nPublicRoute(si no esfalse) addRoutes(rutas de la aplicación)- Catch-all Pug (busca archivo
.pugenpugPath; si no existe llama areq.result('error404', ...)con código 404) - Error handler (llama a
req.result('error500', ...)con código 500; renderiza Pug o devuelve JSON según el tipo de ruta)
sanitizeHtml sanea recursivamente req.body, req.query y req.params. Por defecto no permite etiquetas ni atributos y usa disallowedTagsMode: "recursiveEscape", por lo que el HTML rechazado se conserva como texto escapado. Puede desactivarse con false o configurarse con las opciones de sanitize-html. Se ejecuta después de antiAbuse para que la capa de bloqueo inspeccione primero el payload original.
Helpers en Request/Response
Nota de migración (2.0.4): el helper extendido redirect(...) fue renombrado a redirectResult(...). El método res.redirect(...) mantiene el comportamiento nativo de Express.
Request
req.success(message, next?, options?)— establece notificación de tiposuccessy llama anextreq.error(message, next?, options?)— establece notificación de tipoerrorreq.warning(message, next?, options?)— establece notificación de tipowarningreq.info(message, next?, options?)— establece notificación de tipoinforeq.addPugContext(key, value)— agrega una clave al contexto Pug de la solicitudreq.getPugContext(extra?)— retorna el contexto Pug completoreq.response(uri4pug?, json?, httpCode?)— renderiza vista Pug o devuelve JSON segúnisApiUri;httpCodedefault200req.result(uri4pug, success, message?, json?, httpCode?)— combinajsonResultconresponsereq.redirectResult(uri4redirect, success, message?, json?, httpStatus?)— redirige (o devuelve JSON en rutas API)req.isApiUri—truesi la URL coincide conapiPatternreq.isAuthUri—truesi la URL coincide conauthPatternreq.urlData— objetoUrlDatacon la URL normalizada (ver sección UrlData)req.notification— última notificación establecida conreq.success/error/warning/inforeq.pugContext— objeto de contexto Pug adicionalreq.locale— locale activo (inyectado pori18n)req.locales— lista de locales disponiblesreq.translations— catálogo completo de todos los locales ({ [locale]: { [key]: value } })req.i18n— instanciaI18nreq.__— función de traducción (req.__(key, ...args)) inyectada por i18n vía registro globalreq.getClientIp()— obtiene la dirección IP real del cliente considerando proxiesreq.getClientUserAgent()— obtiene el User-Agent del clientereq.getPath()— obtiene la ruta (path) limpia sin query paramsreq.getHost()— obtiene el valor del header Hostreq.getReferer()— obtiene el valor del header Referer
Response
res.success(message?, json?): Promise<void>— responde{ success: true, message? }con status 200res.error(error?, json?): Promise<void>— responde{ success: false, error? }con status 200res.addPugContext(key, value)res.getPugContext(extra?)res.response(uri4pug?, json?, httpCode?)res.result(uri4pug, success, message?, json?, httpCode?)res.redirect(url)yres.redirect(status, url)— comportamiento nativo de Expressres.redirectResult(uri4redirect, success, message?, json?, httpStatus?)res.getClientIp()— obtiene la dirección IP real del cliente considerando proxiesres.getClientUserAgent()— obtiene el User-Agent del clienteres.getPath()— obtiene la ruta (path) limpia sin query paramsres.getHost()— obtiene el valor del header Hostres.getReferer()— obtiene el valor del header Referer
Contexto Pug base (getPugContext)
Incluye:
url,url_base,urlBaseurl_full,urlFullurl_full_query,urlFullQueryget(req.query)post(req.body)notificationauth(inyectado por middleware externo, p.ej. JWT)localelocales- más lo agregado con
addPugContexty los callbacks deaddPugContextCallback
Orden de prioridad al mezclar: contexto base → callbacks globales → req.pugContext → parámetro extra.
addPugContextCallback
addPugContextCallback((ctx, req, res) => {
ctx.siteName = "Mi App";
ctx.user = (req as any).auth ?? null;
});Los callbacks se almacenan globalmente y se ejecutan en cada solicitud antes de renderizar. El objeto ctx se modifica in-place.
Result Hooks
Los hooks permiten interceptar y reaccionar a las respuestas generadas por result() y redirectResult(). Se registran en InitOptions:
await init({
onResultSuccess: (payload) => {
console.log("✓ Respuesta exitosa", payload);
},
onResultFailure: (payload) => {
console.log("✗ Respuesta fallida", payload);
},
onResultRedirect: (payload) => {
console.log("→ Redirección:", payload.uri4redirect);
},
onResultComplete: (payload) => {
console.log("◉ Respuesta completada (siempre se ejecuta)");
},
});Payload del hook
type ResultHookPayload = {
uri4pug?: string; // vista Pug a renderizar (si aplica)
uri4redirect?: string; // URL de redirección (si aplica)
httpCode: number; // código HTTP de la respuesta
result?: {
success: boolean; // true/false del resultado
message?: string; // mensaje (si success=true)
error?: string; // error (si success=false)
[key: string]: any; // datos adicionales
};
req: Request; // objeto Request de Express
res: Response; // objeto Response de Express
};Ejecución de hooks
onResultSuccessse ejecuta siresult.success === trueonResultFailurese ejecuta siresult.success === falseonResultRedirectse ejecuta sihttpCodees 301 o 302onResultCompletese ejecuta siempre, al finalizar cualquierresult()oredirectResult()
Los hooks se disparan antes de enviar la respuesta al cliente, permitiendo logging, telemetría, o modificaciones finales en req/res.
UrlData
req.urlData contiene la URL normalizada con las siguientes propiedades:
- Estándar:
href,origin,protocol,host,hostname,port,pathname,search,searchParams,hash - Derivadas:
scheme(protocolo sin:),https(boolean),full(origen + pathname),fullWithQuery(full + search),base(origen),path(pathname),qs(query string sin?),fragment(hash sin#)
i18n
i18n(options) usa defaults:
locales: ["en", "es"]defaultLocale: "es"cookie: "lang"queryParameter: "lang"header: "accept-language"directory: "./locales"(si no se pasa)updateFiles: falsesyncFiles: falseautoReload: trueobjectNotation: false
Ejemplo con catálogo estático
import { init } from "@jysperu/load-express";
await init({
i18n: {
staticCatalog: {
es: { hello: "hola" },
en: { hello: "hello" },
},
defaultLocale: "es",
locales: ["es", "en"],
},
});Ruta pública opcional de catálogo i18n
Por defecto init publica una ruta JS en "/i18n.js".
import { init } from "@jysperu/load-express";
await init({
i18nPublicRoute: "/assets/i18n.js",
i18n: {
defaultLocale: "es",
locales: ["es", "en"],
staticCatalog: {
es: { hello: "hola" },
en: { hello: "hello" },
},
},
});Ejemplo de JS resultante de GET /i18n.js:
window.locale = "en";
window.i18n = { hello: "hello", welcome_user: "welcome {{name}}" };
window.__ = function __(key, ...args) {
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const table = window?.i18n ?? {};
const source = table[key];
let str = typeof source === "string" ? source : key;
if (args.length === 1 && args[0] && typeof args[0] === "object" && !Array.isArray(args[0])) {
const replacements = args[0];
for (const [k, value] of Object.entries(replacements)) {
const token = new RegExp(`{{${escapeRegExp(k)}}}`, "g");
str = str.replace(token, String(value));
}
} else if (args.length > 0) {
for (const arg of args) {
str = str.replace(/%[sd]/, String(arg));
}
}
return str;
};La suite incluye una prueba de ejecución real del script devuelto por /i18n.js:
- Evalúa el JS en un sandbox.
- Verifica
window.locale,window.i18nywindow.__. - Comprueba traducción de claves existentes, reemplazos
{{name}}y fallback de clave faltante.
Puedes ejecutarla de forma aislada con:
npm test -- tests/i18nPublicRoute.test.tsSeguridad y control
secureHeaders(options?)
Defaults principales:
X-XSS-Protection: 1; mode=blockX-Content-Type-Options: nosniffX-Frame-Options: SAMEORIGINReferrer-Policy: no-referrerStrict-Transport-Security: max-age=15552000; includeSubDomainsCross-Origin-Opener-Policy: same-originCross-Origin-Resource-Policy: same-site
rateLimit(options?)
Defaults:
windowMs: 60000max: 100setHeaders: truedenyStatusCode: 429denyMessage: "Too many requests"
antiAbuse(options?)
Bloquea por método/IP/UA/path, patrones sospechosos, longitud de URL y tamaño de body.
Defaults clave:
allowedMethods: ["GET","POST","PUT","DELETE","PATCH","HEAD","OPTIONS"]maxUrlLength: 2048maxContentLength: 10MBdenyStatusCode: 403
Los patrones se evaluan sobre la URL (raw y decodificada) y el body, cubriendo ataques con y sin URL-encoding. Los RegExp se compilan una sola vez al crear el middleware para minimizar el impacto en rendimiento.
Patrones incluidos por defecto:
- SQL Injection:
UNION SELECT,SELECT FROM,INSERT INTO,UPDATE SET,DELETE FROM,DROP TABLE,INTO OUTFILE/DUMPFILE,LOAD_FILE(),base64_encode/decode(),benchmark(),exec()/xp_cmdshell,concat(),sleep(),WAITFOR DELAY(MSSQL),pg_sleep()(PostgreSQL) - XSS:
<script>,<iframe>,<object>,<embed>,javascript:, event handlers inline (onX=) - Path traversal:
../y variantes encoded (%2e%2e%2f,%2e%2e\) - Null byte:
\x00y%00 - CRLF injection:
%0a,%0d - Command injection: operadores shell (`; | & ``) seguidos de comandos comunes
Cuando una solicitud es bloqueada, el código de bloqueo se devuelve en json.code y el mensaje en json.reason. Los códigos posibles están en AntiAbuseReasons:
BLOCKED_USER_AGENT,BLOCKED_IP,BLOCKED_METHOD,BLOCKED_PATHURL_TOO_LONG,CONTENT_TOO_LARGE,SUSPICIOUS_PATTERN
Opción skip
Todos los middlewares (secureHeaders, rateLimit, antiAbuse, timeout, cors, json, urlencoded, cookieParser, compression, minify) aceptan una función skip para omitir el middleware condicionalmente:
init({
rateLimit: { skip: (req) => req.path === "/health" },
antiAbuse: { skip: (req) => req.ip === "127.0.0.1" },
});En antiAbuse, skip también puede reemplazar arrays de patrones por request (solo el array sustituido se recompila; el resto usa los patrones pre-compilados):
antiAbuse({
suspiciousPatterns: [],
skip: (req, opts) => {
if (req.path === "/strict") opts.suspiciousPatterns = [/union\s+select/i];
return false;
},
});Wrappers adicionales
timeout(...)(default30s)cors(...)json(...)(defaultlimit: "50mb")urlencoded(...)(defaultlimit: "50mb", extended: true)cookieParser(...)compression(...)minify(...)(defaultcache: true,jsonMatch: false)
El parámetro cache de minify acepta también una función para determinar la carpeta de caché por solicitud:
init({
minify: { cache: (req) => (req.isApiUri ? false : "./cache") },
});Ejemplo completo
import { init, addPugContextCallback } from "@jysperu/load-express";
addPugContextCallback((ctx, req) => {
ctx.siteName = "Mi App";
ctx.user = (req as any).auth ?? null;
});
await init({
i18n: {
defaultLocale: "es",
locales: ["es", "en"],
staticCatalog: {
es: { hello: "hola" },
en: { hello: "hello" },
},
},
secureHeaders: true,
rateLimit: { max: 120, windowMs: 60_000 },
antiAbuse: true,
timeout: "5s",
addRoutes(app) {
app.get("/api/v1/hello", (req, res) => {
res.success(req.__?.("hello"));
});
},
});Acceso al estado interno
Para casos avanzados donde necesites acceso directo a las instancias de Express:
import { ExpressApp, ExpressServerInstance } from "@jysperu/load-express";
// Obtener la instancia Express actual (puede ser null si no está inicializada)
const app = ExpressApp();
if (app) {
// Trabajar con la instancia Express directamente
app.use("/custom", myMiddleware);
}
// Obtener la instancia del servidor HTTP (puede ser null)
const server = ExpressServerInstance();
if (server) {
// Trabajar con el servidor directamente
console.log("Servidor corriendo en:", server.address());
}Nota de seguridad: Las funciones getter ExpressApp() y ExpressServerInstance() proporcionan acceso controlado y seguro al estado interno, reemplazando el acceso directo a propiedades app y server de versiones anteriores.
Observaciones de compatibilidad
init()es asíncrona (Promise<Express | false>): siempre usaawait init(...).init()es singleton: llamadas posteriores reutilizan la misma instanciaapp.- Para reiniciar la instancia en tests/procesos controlados, usar
close(). urlDatade request normaliza URL (full,fullWithQuery,qs,scheme, etc.).
Scripts
npm run test
npm run test:watch
npm run buildLicencia
MIT © JYS Perú
