Mejores prácticas en scripts de Node.js
Un resumen condensado de las 25 mejores prácticas más importantes extraídas de todas las páginas de esta sección.
- Usa el Meta-Paquete typescript-eslint: Instala
typescript-eslint(el meta-paquete) en lugar de separados@typescript-eslint/parsery@typescript-eslint/eslint-pluginpara obtenertseslint.config()y el parser incluido conectado consistentemente. - Habilita parserOptions.project para Reglas de Tipo: Las reglas conscientes de tipos como
no-floating-promisesyno-misused-promisesno funcionan silenciosamente sinparserOptions.project; establécelo (ytsconfigRootDir: import.meta.dirname) para que las rutas se resuelvan relativas a la configuración, no al cwd. - Pon eslint-config-prettier Último, Aísla ignores: En configuración plana,
eslint-config-prettierdebe ser la entrada final o sus deshabilitaciones de reglas se anulan, y un bloque{ ignores: […] }solo actúa como un ignore global cuando es la única clave en su objeto — mezclarlo conruleslo convierte en un filtro por archivo. - Elige el Sistema de Módulos Explícitamente: Node elige por archivo:
.mjses siempre ESM,.cjses siempre CommonJS, y.jssimple sigue el campo"type"depackage.jsonmás cercano (predeterminado a CommonJS); cambiar"type": "module"convierte cada.jsdebajo y requiere renombrar retrocesos CJS a.cjs. - Escribe Extensiones .js Explícitas en ESM: ESM nativo lanza
ERR_MODULE_NOT_FOUNDsin la extensión —import { helper } from "./utils.js"incluso cuando la fuente esutils.ts— porque bajo"module": "NodeNext"el especificador modela la ruta runtime emitida. - Reconstruye __dirname en ESM:
__dirname,__filename, yrequireno existen en el alcance ESM —const __dirname = path.dirname(fileURLToPath(import.meta.url))— en lugar de copiar y pegar código CJS que lanza silenciosamente. - Usa NodeNext en tsconfig.json: Establece tanto
"module": "NodeNext"como"moduleResolution": "NodeNext"para que TypeScript modele fielmente la resolución real de Node — incluyendo condicionesexports,.mts/.cts, y la extensión.jsrequerida. - Siempre Escucha por 'error':
EventEmittertrata'error'especialmente — emitiéndolo sin listeners registrados bloquea el proceso con una excepción no capturada, así que adjunta un manejador (incluso solo registrando) en cada emisor que posees. - Almacena Referencias de Manejadores para off:
emitter.off(event, fn)solo elimina la referencia exacta de función que registraste —const handler = () => { ... }; emitter.on("data", handler); emitter.off("data", handler)— las funciones flecha anónimas nunca coinciden, así que guarda el manejador en una variable nombrada. - Respeta Advertencias de MaxListeners: El
MaxListenerspredeterminado es 10 y la advertencia se dispara una vez a las 11, que es una señal fácil de perder de fuga; arregla el sobre-registro, aumenta el límite intencionalmente consetMaxListeners(), o usaEventEmitter.defaultMaxListeners. - Limita el Tamaño del Cuerpo de Solicitud HTTP: La concatenación manual de cuerpo en
node:httpsin límite de bytes es un vector de ataque OOM; acumula en un contador de longitud y responde 413 una vez que cruces un límite (Express usaexpress.json({ limit }), Fastify tienebodyLimit). - Envuelve Manejadores Async de Express 4: Express 4 silenciosamente traga errores lanzados y promesas rechazadas de manejadores async —
app.get("/", (req, res, next) => { handleAsync(req, res).catch(next) })— o actualiza a Express 5 que reenvía al middleware de error de forma nativa. - Ten cuidado con Estilos de Respuesta Express vs Fastify: Express ignora valores de retorno y requiere
res.send/res.json; Fastify envía lo que devuelves desde el manejador — mezclar los dos estilos (devolver datos en Express, llamar areply.senden Fastify) produce respuestas colgadas o duplicadas. - Instala Node a través de un Gestor de Versiones: nvm, fnm, o Volta instalan en tu directorio de inicio, omiten
sudocompletamente, y te permiten cambiar versiones de Node por proyecto; ejecutar el instalador oficial mássudo npm install -glleva directamente a miseriaEACCES. - Objetiva Versiones LTS Numeradas Pares: Las versiones LTS (mayores numeradas pares como 20, 22) reciben 30 meses de soporte; las versiones Current numeradas impares mueren después de aproximadamente 6 meses, así que siempre elige el LTS más reciente para scripts y servidores.
- Fija packageManager a través de Corepack: Establece
"packageManager": "pnpm@x.y.z"enpackage.jsonpara que Corepack (incluido con Node 18.17+) aplique la herramienta exacta y la versión en cada colaborador y ejecutor CI, eliminando la deriva de instalación "funciona en mi máquina". - Arregla Dependencias Fantasma Después de la Migración: El
node_modulesaplanado y hoisted de npm oculta deps fantasma (importaciones no enpackage.json); la migración al layout estrictamente simbólico de pnpm las suface como errores reales — arréglalo añadiendo las dependencias, no cambiando anode-linker=hoisted. - Analiza argv Con node:util parseArgs: El
parseArgsintegrado maneja opciones sin agregar una dependencia —const { values } = parseArgs({ args: process.argv.slice(2), options: { port: { type: "string", default: "3000" } } })— y salta automáticamente las entradas de nodo y ruta de script. - Separa Banderas de Script npm Con --:
npm run cli --flagpasa--flaga npm mismo, no a tu script; usanpm run cli -- --flagpara que la bandera llegue al comando subyacente (pnpm y yarn se comportan de la misma manera). - Usa tsx para Dev, JavaScript Compilado para Prod:
tsxinicia en ~100ms usando esbuild y es ideal para scripts locales; la producción debe ejecutar JavaScript compilado víatsc+nodepara que no haya dependencia de runner y el costo de inicio se mantenga plano. - Ejecutores de TS No Verifican Tipos:
tsx,ts-node, ynode --experimental-strip-typestodos descartan tipos y entregan JavaScript a V8 — ninguno de ellos captura errores de tipo en tiempo de ejecución, así que ejecutatsc --noEmiten CI para seguridad real. - Instala @types/node: Sin
@types/nodecomo devDependency,process,Buffer, y cada importaciónnode:*sonany, matando el autocompletado y ocultando bugs reales detrás de coerciones implícitas-any silenciosas. - Pasa "utf8" a readFile:
fs.readFile(path)sin una codificación devuelve unBuffer, no una cadena —const text = await readFile("config.json", "utf8")— así que el análisis de downstream.split/regex/JSON funciona sin un búfer sorpresa. - Usa process.exitCode, No process.exit:
process.exit()termina inmediatamente y trunca stdout pendiente o escrituras async —process.exitCode = 1; return— dejando que el event loop se drene preserva logs mientras aún falla el script. - Fuerza Colores Chalk en CI: Chalk suprime colores cuando
process.stdout.isTTYes false, que es el predeterminado en la mayoría de entornos CI; estableceFORCE_COLOR=1(o equivalente) en el env de CI si quieres salida de log coloreada y recuerda que Chalk v5 es solo ESM.