Antes de entrar en Node.js, necesitas tener claros los fundamentos del lenguaje. Esta sección cubre el JavaScript moderno (ES2020+) que usarás a diario.
Variables: const, let y por qué olvidar var
Regla de oro: usa siempre const. Solo cambia a let cuando el linter te avise que necesitas reasignar. Nunca uses var.
Tipos de datos
Template literals (strings modernos)
Desestructuración: la forma elegante de extraer datos
Spread y Rest operator (...)
Funciones: declaraciones, expresiones y arrow functions
Arrays: los métodos que usarás todos los días
Objetos modernos
Clases (ES2022+ con campos privados)
El Event Loop en 30 segundos
Este es el concepto más importante de JavaScript y Node.js. JavaScript es single-threaded — solo puede ejecutar una cosa a la vez. Entonces, ¿cómo maneja miles de peticiones simultáneas?
┌─────────────────────────┐
│ Call Stack │ ← Tu código se ejecuta aquí, uno a uno
└────────────┬────────────┘
│ cuando el stack está vacío...
┌────────────▼────────────┐
│ Event Loop │ ← Revisa si hay callbacks listos
└────────────┬────────────┘
│
┌────────────▼────────────┐
│ Callback Queue │ ← Callbacks de timers, I/O, etc.
└─────────────────────────┘
La clave: Las operaciones de I/O (leer archivos, consultas a BD, peticiones HTTP) son no bloqueantes. Node.js las delega al sistema operativo y continúa procesando otras peticiones. Cuando terminan, el callback entra a la cola. Por eso Node.js es tan eficiente para APIs.
El objeto this y por qué las arrow functions importan
Manejo de errores síncronos
Con estos fundamentos de JavaScript bien asentados, ya estás listo para aprovechar Node.js al máximo. Todo lo que viene a continuación los usará constantemente.
1. Fundamentos y Ecosistema Moderno
¿Qué es Node.js en 2025?
Node.js es un runtime de JavaScript construido sobre el motor V8 de Chrome. Hoy en día, Node.js 22+ viene con soporte nativo para TypeScript (con el flag --experimental-strip-types), ES Modules por defecto, y una API de fetch global sin necesidad de paquetes externos.
Instalación recomendada: fnm (Fast Node Manager)
Nunca instales Node.js directamente en tu sistema. Usa un gestor de versiones.
Inicializar un proyecto moderno
Edita tu package.json para habilitarlo como módulo ES:
Tip Pro:"type": "module" activa ES Modules en todo el proyecto. node --watch es el hot-reload nativo de Node (no necesitas nodemon).
2. Módulos ES Modules vs CommonJS
ES Modules (la forma correcta en 2025)
Importaciones dinámicas (lazy loading)
Importar JSON nativo (Node 22+)
¿Cuándo usar CommonJS?
Solo cuando trabajas con librerías legacy que no tienen soporte ESM. En ese caso, usa la extensión .cjs:
3. Manejo Asíncrono Moderno
El problema con los callbacks (nunca hagas esto)
Async/Await: la forma correcta
Tip: Usa node:fs/promises (con el prefijo node:) en lugar de fs/promises. El prefijo node: deja claro que es un módulo nativo y mejora la carga.
Manejo de errores con async/await
Operaciones en paralelo
Streams: procesando datos grandes eficientemente
4. Arquitectura Modular y Estructura de Proyecto
Una buena arquitectura es la diferencia entre un proyecto mantenible y uno que se vuelve inmanejable.
Petición HTTP
↓
Router ← Define rutas y métodos HTTP
↓
Controller ← Maneja req/res, llama al servicio
↓
Service ← Lógica de negocio pura
↓
Repository ← Acceso a datos (DB, APIs externas)
↓
Base de Datos
Ejemplo: Módulo de Usuarios completo
src/features/usuarios/usuarios.repository.js
src/features/usuarios/usuarios.service.js
src/features/usuarios/usuarios.controller.js
src/features/usuarios/usuarios.router.js
5. APIs REST con Express y mejores prácticas
Instalación
src/app.js — Configuración central de Express
src/index.js — Punto de entrada
6. Validación, Errores y Seguridad
Errores personalizados
src/lib/errors.js
Middleware de errores centralizado
src/middleware/error.middleware.js
Validación con Zod (la mejor librería de validación)
src/features/usuarios/usuarios.schema.js
src/middleware/validate.middleware.js
7. Base de Datos con ORM moderno (Prisma)
prisma/schema.prisma
src/lib/database.js
Migraciones
8. Testing Profesional
Node.js 22 incluye un test runner nativo. No necesitas Jest para la mayoría de los casos.
Tests unitarios
tests/unit/usuarios.service.test.js
Tests de integración con supertest
tests/integration/usuarios.test.js
9. Variables de Entorno y Configuración
.env
.env.example (commitear esto, nunca el .env)
src/config/index.js — Validar variables de entorno al iniciar
Por qué esto importa: Si falta una variable de entorno crítica, tu app fallará al arrancar con un mensaje claro, en lugar de fallar silenciosamente horas después en producción.
10. Logging y Observabilidad
Pino: el logger más rápido para Node.js
src/lib/logger.js
Uso
11. Performance y Buenas Prácticas Avanzadas
Worker Threads: CPU-intensive tasks
Caching con Map (en memoria, simple y efectivo)
Private class fields (ES2022+)
Evitar errores no capturados
12. Contenedores y Deployment
Dockerfile optimizado para Node.js
docker-compose.yml para desarrollo local
.gitignore esencial
Checklist Final: ¿Tu API está lista para producción?
Variables de entorno validadas al arrancar
Manejo centralizado de errores
Logging estructurado (sin console.log)
Rate limiting habilitado
Helmet.js para headers de seguridad
Validación de inputs con Zod
Passwords hasheados con bcrypt (rounds >= 12)
JWT con expiración corta
Tests unitarios e integración
Health check endpoint
Cierre graceful del servidor
Handlers para uncaughtException y unhandledRejection
// ❌ var tiene scope de función y se puede redeclarar — evítalo siemprevar nombre = 'Ana';var nombre = 'Luis'; // No hay error... pero es un bug esperando ocurrir// ✅ const: para valores que no se reasignan (úsalo por defecto)const PI = 3.14;const usuario = { nombre: 'Ana', edad: 28 }; // el objeto es mutable, la referencia nousuario.edad = 29; // ✅ esto sí está permitido// ✅ let: solo cuando necesitas reasignarlet contador = 0;contador += 1; // OK
javascript
// Primitivosconst texto = 'Hola mundo'; // stringconst numero = 42; // numberconst decimal = 3.14; // number (no hay float separado)const activo = true; // booleanconst vacio = null; // null — ausencia intencional de valorconst sinDef = undefined; // undefined — variable sin asignarconst id = Symbol('id'); // symbol — único e irrepetibleconst grande = 9007199254740993n; // bigint — enteros enormes// Referenciaconst array = [1, 2, 3];const objeto = { clave: 'valor' };const funcion = () => {};
// Declaración de función (hoisted — disponible antes de declararse)function saludar(nombre) { return `Hola, ${nombre}`;}// Expresión de funciónconst despedir = function(nombre) { return `Adiós, ${nombre}`;};// Arrow function — sintaxis moderna, no tiene su propio 'this'const multiplicar = (a, b) => a * b;// Con cuerpo de bloque cuando hay más de una líneaconst procesarUsuario = (usuario) => { const nombre = usuario.nombre.trim(); const email = usuario.email.toLowerCase(); return { ...usuario, nombre, email };};// Parámetros por defectoconst saludarConRol = (nombre, rol = 'visitante') => { return `Hola ${nombre}, eres ${rol}`;};
javascript
const productos = [ { nombre: 'Laptop', precio: 999, activo: true }, { nombre: 'Mouse', precio: 29, activo: false }, { nombre: 'Teclado', precio: 79, activo: true },];// map — transformar cada elemento → nuevo arrayconst nombres = productos.map(p => p.nombre);// ['Laptop', 'Mouse', 'Teclado']// filter — filtrar por condición → nuevo arrayconst activos = productos.filter(p => p.activo);// [{ nombre: 'Laptop', ...}, { nombre: 'Teclado', ...}]// find — encontrar el primero que cumplaconst laptop = productos.find(p => p.nombre === 'Laptop');// reduce — acumular un valorconst total = productos.reduce((acc, p) => acc + p.precio, 0);// 1107// some / everyconst hayCaros = productos.some(p => p.precio > 500); // trueconst todosCaro = productos.every(p => p.precio > 500); // false// Encadenado — el patrón más común en Node.jsconst totalActivos = productos .filter(p => p.activo) .reduce((acc, p) => acc + p.precio, 0);// 1078// flat y flatMap (muy útiles con datos anidados)const categorias = [['js', 'ts'], ['python', 'go']];categorias.flat(); // ['js', 'ts', 'python', 'go']
javascript
// Shorthand propertiesconst nombre = 'Lucía';const edad = 27;const obj = { nombre, edad }; // En lugar de { nombre: nombre, edad: edad }// Computed property namesconst campo = 'email';const datos = { [campo]: 'lucia@example.com' }; // { email: 'lucia@example.com' }// Optional chaining — evita "Cannot read property of undefined"const usuario = { perfil: { ciudad: 'Barcelona' } };const ciudad = usuario?.perfil?.ciudad; // 'Barcelona'const cp = usuario?.perfil?.codigoPostal; // undefined (no lanza error)const metodo = usuario?.getPermisos?.(); // funciona con métodos también// Nullish coalescing — valor por defecto solo si es null/undefinedconst puerto = process.env.PORT ?? 3000; // 3000 si PORT no existeconst cero = 0 ?? 'default'; // 0 (no es null/undefined)const falsy = 0 || 'default'; // 'default' (|| también reacciona a 0, "")
javascript
class Cuenta { // Campos privados con # #saldo; #titular; constructor(titular, saldoInicial = 0) { this.#titular = titular; this.#saldo = saldoInicial; } depositar(cantidad) { if (cantidad <= 0) throw new Error('La cantidad debe ser positiva'); this.#saldo += cantidad; return this; // Permite encadenamiento: cuenta.depositar(100).depositar(50) } retirar(cantidad) { if (cantidad > this.#saldo) throw new Error('Saldo insuficiente'); this.#saldo -= cantidad; return this; } // Getter — accede como propiedad, no como método get saldo() { return this.#saldo; } get titular() { return this.#titular; } // Método estático — se llama en la clase, no en la instancia static crearCuentaNueva(titular) { return new Cuenta(titular, 0); } toString() { return `Cuenta de ${this.#titular}: ${this.#saldo}€`; }}const cuenta = Cuenta.crearCuentaNueva('Ana');cuenta.depositar(500).depositar(200).retirar(100);console.log(cuenta.saldo); // 600console.log(`${cuenta}`); // 'Cuenta de Ana: 600€'// cuenta.#saldo // ❌ SyntaxError — campo privado
javascript
console.log('1 — Síncrono');setTimeout(() => console.log('2 — Timer (macro-task)'), 0);Promise.resolve().then(() => console.log('3 — Microtask'));console.log('4 — Síncrono');// Output:// 1 — Síncrono// 4 — Síncrono// 3 — Microtask ← Las microtasks (Promises) van antes que los timers// 2 — Timer (macro-task)
javascript
class Temporizador { constructor() { this.segundos = 0; } // ❌ MALO — 'this' dentro del callback apunta a undefined (o global) iniciarMal() { setInterval(function() { this.segundos++; // TypeError: Cannot set properties of undefined }, 1000); } // ✅ BUENO — Arrow function hereda 'this' del contexto padre iniciar() { setInterval(() => { this.segundos++; // 'this' es el Temporizador }, 1000); }}
javascript
// try/catch/finallyfunction dividir(a, b) { if (b === 0) throw new Error('División por cero'); return a / b;}try { const resultado = dividir(10, 0);} catch (error) { console.error(error.message); // 'División por cero'} finally { // Se ejecuta SIEMPRE, haya error o no console.log('Operación completada');}// Tipos de error nativosthrow new TypeError('Se esperaba un número');throw new RangeError('El valor debe estar entre 0 y 100');throw new SyntaxError('JSON malformado');
// utils/math.jsexport function sumar(a, b) { return a + b;}export const PI = 3.14159;// Default exportexport default function calcular(operacion, a, b) { const ops = { sumar, restar: (a, b) => a - b }; return ops[operacion]?.(a, b) ?? null;}
javascript
// main.jsimport calcular, { sumar, PI } from './utils/math.js'; // ← la extensión .js es OBLIGATORIAconsole.log(sumar(2, 3)); // 5console.log(PI); // 3.14159console.log(calcular('sumar', 10, 5)); // 15
javascript
// Solo carga el módulo cuando realmente lo necesitasasync function procesarArchivo(tipo) { if (tipo === 'csv') { const { parsearCSV } = await import('./parsers/csv.js'); return parsearCSV; } const { parsearJSON } = await import('./parsers/json.js'); return parsearJSON;}
javascript
import config from './config.json' with { type: 'json' };console.log(config.puerto); // Sin necesidad de fs.readFileSync
javascript
// legacy.cjs ← nota la extensiónconst express = require('express');module.exports = { app: express() };
javascript
// ❌ MALO - Callback hellfs.readFile('archivo.txt', (err, data) => { if (err) { db.query('INSERT INTO errores VALUES (?)', [err.message], (err2) => { // ...más anidamiento infinito }); }});
// Patrón: wrapper para evitar try/catch repetitivoasync function intentar(promesa) { try { const resultado = await promesa; return [null, resultado]; } catch (error) { return [error, null]; }}// Uso limpioasync function obtenerUsuario(id) { const [error, usuario] = await intentar(db.findById(id)); if (error) { console.error('Error al obtener usuario:', error.message); return null; } return usuario;}
javascript
// ❌ LENTO - Secuencial (espera cada uno)const usuario = await obtenerUsuario(id);const pedidos = await obtenerPedidos(id);const facturas = await obtenerFacturas(id);// ✅ RÁPIDO - Paralelo (todos al mismo tiempo)const [usuario, pedidos, facturas] = await Promise.all([ obtenerUsuario(id), obtenerPedidos(id), obtenerFacturas(id),]);// Con manejo individual de erroresconst resultados = await Promise.allSettled([ obtenerUsuario(id), obtenerPedidos(id),]);resultados.forEach(({ status, value, reason }) => { if (status === 'fulfilled') console.log(value); else console.error('Falló:', reason);});
javascript
import { createReadStream, createWriteStream } from 'node:fs';import { pipeline } from 'node:stream/promises';import { createGzip } from 'node:zlib';// Comprimir un archivo gigante SIN cargar todo en memoriaasync function comprimirArchivo(entrada, salida) { await pipeline( createReadStream(entrada), createGzip(), createWriteStream(salida) ); console.log('Archivo comprimido exitosamente');}await comprimirArchivo('datos-grandes.csv', 'datos-grandes.csv.gz');
javascript
// Solo habla con la base de datosimport { prisma } from '../../lib/database.js';export class UsuariosRepository { async findById(id) { return prisma.usuario.findUnique({ where: { id } }); } async findByEmail(email) { return prisma.usuario.findUnique({ where: { email } }); } async create(datos) { return prisma.usuario.create({ data: datos }); } async update(id, datos) { return prisma.usuario.update({ where: { id }, data: datos }); } async delete(id) { return prisma.usuario.delete({ where: { id } }); }}
javascript
// Solo lógica de negocio, sin saber nada de HTTPimport bcrypt from 'bcrypt';import { UsuariosRepository } from './usuarios.repository.js';import { NotFoundError, ConflictError } from '../../lib/errors.js';const repo = new UsuariosRepository();export class UsuariosService { async obtenerPorId(id) { const usuario = await repo.findById(id); if (!usuario) throw new NotFoundError(`Usuario ${id} no encontrado`); return usuario; } async crear(datos) { const existe = await repo.findByEmail(datos.email); if (existe) throw new ConflictError('El email ya está registrado'); const hash = await bcrypt.hash(datos.password, 12); const { password, ...usuario } = await repo.create({ ...datos, password: hash, }); return usuario; // Nunca devuelves el password }}
javascript
// Solo maneja req/res, delega al servicioimport { UsuariosService } from './usuarios.service.js';const service = new UsuariosService();export async function obtenerUsuario(req, res, next) { try { const usuario = await service.obtenerPorId(req.params.id); res.json({ data: usuario }); } catch (error) { next(error); // Delega al middleware de errores }}export async function crearUsuario(req, res, next) { try { const usuario = await service.crear(req.body); res.status(201).json({ data: usuario }); } catch (error) { next(error); }}
javascript
import { Router } from 'express';import { validate } from '../../middleware/validate.middleware.js';import { authMiddleware } from '../../middleware/auth.middleware.js';import { crearUsuarioSchema } from './usuarios.schema.js';import { obtenerUsuario, crearUsuario } from './usuarios.controller.js';export const usuariosRouter = Router();usuariosRouter.get('/:id', authMiddleware, obtenerUsuario);usuariosRouter.post('/', validate(crearUsuarioSchema), crearUsuario);
bash
npm install expressnpm install -D @types/express # Solo si usas TypeScript
javascript
import express from 'express';import helmet from 'helmet';import cors from 'cors';import rateLimit from 'express-rate-limit';import { usuariosRouter } from './features/usuarios/usuarios.router.js';import { productosRouter } from './features/productos/productos.router.js';import { errorMiddleware } from './middleware/error.middleware.js';import { logger } from './lib/logger.js';export function crearApp() { const app = express(); // Seguridad app.use(helmet()); app.use(cors({ origin: process.env.CORS_ORIGIN || '*' })); // Rate limiting const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutos max: 100, message: { error: 'Demasiadas peticiones, intenta más tarde' }, }); app.use('/api', limiter); // Body parsing app.use(express.json({ limit: '10kb' })); // Limitar tamaño para evitar ataques // Request logging app.use((req, _res, next) => { logger.info({ method: req.method, url: req.url, ip: req.ip }); next(); }); // Rutas app.use('/api/v1/usuarios', usuariosRouter); app.use('/api/v1/productos', productosRouter); // Health check app.get('/health', (_req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }); }); // 404 app.use((_req, res) => { res.status(404).json({ error: 'Ruta no encontrada' }); }); // Error handler (siempre al final) app.use(errorMiddleware); return app;}
javascript
import { crearApp } from './app.js';import { conectarDB } from './lib/database.js';import { config } from './config/index.js';import { logger } from './lib/logger.js';async function iniciar() { await conectarDB(); const app = crearApp(); const servidor = app.listen(config.puerto, () => { logger.info(`🚀 Servidor corriendo en http://localhost:${config.puerto}`); }); // Cierre graceful: terminar conexiones activas antes de apagar const cerrarGracefully = async (señal) => { logger.info(`Recibida señal ${señal}, cerrando servidor...`); servidor.close(async () => { await prisma.$disconnect(); logger.info('Servidor cerrado correctamente'); process.exit(0); }); }; process.on('SIGTERM', cerrarGracefully); process.on('SIGINT', cerrarGracefully);}iniciar().catch((error) => { console.error('Error al iniciar:', error); process.exit(1);});
import { z } from 'zod';export const crearUsuarioSchema = z.object({ nombre: z.string().min(2).max(100).trim(), email: z.string().email().toLowerCase(), password: z .string() .min(8) .regex(/[A-Z]/, 'Debe contener al menos una mayúscula') .regex(/[0-9]/, 'Debe contener al menos un número'), rol: z.enum(['admin', 'usuario']).default('usuario'),});export const actualizarUsuarioSchema = crearUsuarioSchema.partial().omit({ password: true,});
javascript
import { ZodError } from 'zod';import { ValidationError } from '../lib/errors.js';export function validate(schema) { return (req, _res, next) => { const resultado = schema.safeParse(req.body); if (!resultado.success) { const detalles = resultado.error.issues.map((issue) => ({ campo: issue.path.join('.'), mensaje: issue.message, })); return next(new ValidationError('Datos inválidos', detalles)); } req.body = resultado.data; // Datos limpios y transformados next(); };}
bash
npm install prisma @prisma/clientnpx prisma init
prisma
generator client { provider = "prisma-client-js"}datasource db { provider = "postgresql" url = env("DATABASE_URL")}model Usuario { id String @id @default(cuid()) nombre String email String @unique password String rol Rol @default(USUARIO) creadoEn DateTime @default(now()) @map("creado_en") pedidos Pedido[] @@map("usuarios")}model Pedido { id String @id @default(cuid()) total Decimal @db.Decimal(10, 2) estado Estado @default(PENDIENTE) usuario Usuario @relation(fields: [usuarioId], references: [id]) usuarioId String @map("usuario_id") creadoEn DateTime @default(now()) @map("creado_en") @@map("pedidos")}enum Rol { ADMIN USUARIO}enum Estado { PENDIENTE PROCESANDO COMPLETADO CANCELADO}
javascript
import { PrismaClient } from '@prisma/client';import { logger } from './logger.js';// Singleton: una sola instancia en toda la appconst globalForPrisma = globalThis;export const prisma = globalForPrisma.prisma ?? new PrismaClient({ log: [ { emit: 'event', level: 'query' }, { emit: 'event', level: 'error' }, ], });prisma.$on('error', (e) => logger.error({ msg: 'DB Error', error: e.message }));if (process.env.NODE_ENV !== 'production') { globalForPrisma.prisma = prisma;}export async function conectarDB() { await prisma.$connect(); logger.info('✅ Conectado a la base de datos');}
bash
# Crear una migraciónnpx prisma migrate dev --name agregar-campo-telefono# Aplicar en producciónnpx prisma migrate deploy# Ver el estadonpx prisma migrate status
bash
# El test runner nativonode --test# Con coberturanode --test --experimental-test-coverage
javascript
import { describe, it, before, after, mock } from 'node:test';import assert from 'node:assert/strict';// Mock del repositorio para no tocar la DBconst mockRepo = { findByEmail: mock.fn(), create: mock.fn(),};// Inyección de dependencia para testsdescribe('UsuariosService', () => { describe('crear()', () => { it('debe crear un usuario exitosamente', async () => { mockRepo.findByEmail.mock.mockImplementationOnce(() => null); mockRepo.create.mock.mockImplementationOnce((datos) => ({ id: '123', ...datos, })); const service = new UsuariosServiceTestable(mockRepo); const usuario = await service.crear({ nombre: 'Ana García', email: 'ana@example.com', password: 'Segura123', }); assert.equal(usuario.email, 'ana@example.com'); assert.equal(usuario.password, undefined); // No devuelve el password }); it('debe lanzar ConflictError si el email ya existe', async () => { mockRepo.findByEmail.mock.mockImplementationOnce(() => ({ id: '456' })); const service = new UsuariosServiceTestable(mockRepo); await assert.rejects( () => service.crear({ email: 'duplicado@example.com', password: 'Test123' }), { name: 'ConflictError' } ); }); });});
bash
npm install -D supertest
javascript
import { describe, it, before, after } from 'node:test';import assert from 'node:assert/strict';import request from 'supertest';import { crearApp } from '../../src/app.js';import { prisma } from '../../src/lib/database.js';const app = crearApp();describe('POST /api/v1/usuarios', () => { after(async () => { await prisma.usuario.deleteMany({ where: { email: { contains: 'test' } } }); await prisma.$disconnect(); }); it('debe crear un usuario y devolver 201', async () => { const res = await request(app) .post('/api/v1/usuarios') .send({ nombre: 'Test User', email: 'test@example.com', password: 'Segura123', }); assert.equal(res.status, 201); assert.ok(res.body.data.id); assert.equal(res.body.data.email, 'test@example.com'); assert.equal(res.body.data.password, undefined); }); it('debe devolver 400 con datos inválidos', async () => { const res = await request(app) .post('/api/v1/usuarios') .send({ email: 'no-es-email' }); assert.equal(res.status, 400); assert.equal(res.body.code, 'VALIDATION_ERROR'); });});
import { z } from 'zod';const envSchema = z.object({ NODE_ENV: z.enum(['development', 'test', 'production']).default('development'), PORT: z.coerce.number().default(3000), DATABASE_URL: z.string().url(), JWT_SECRET: z.string().min(32, 'JWT_SECRET debe tener al menos 32 caracteres'), CORS_ORIGIN: z.string().default('*'),});const resultado = envSchema.safeParse(process.env);if (!resultado.success) { console.error('❌ Variables de entorno inválidas:'); console.error(resultado.error.format()); process.exit(1); // Fallar rápido y fuerte}export const config = Object.freeze(resultado.data);
bash
npm install pino pino-pretty
javascript
import pino from 'pino';import { config } from '../config/index.js';export const logger = pino({ level: config.NODE_ENV === 'production' ? 'info' : 'debug', // En desarrollo: logs bonitos y legibles // En producción: JSON estructurado para herramientas como Datadog/Loki transport: config.NODE_ENV !== 'production' ? { target: 'pino-pretty', options: { colorize: true } } : undefined, // Campos base en todos los logs base: { env: config.NODE_ENV, pid: process.pid, }, // Ocultar datos sensibles redact: ['req.headers.authorization', 'body.password', 'body.token'],});
javascript
import { logger } from './lib/logger.js';// ✅ Log estructurado (searchable en producción)logger.info({ userId: '123', action: 'crear_pedido' }, 'Pedido creado');// ✅ Error con contextologger.error({ error: err.message, stack: err.stack }, 'Fallo al procesar pago');// ❌ Nunca en producciónconsole.log('algo pasó');
javascript
import { Worker, isMainThread, parentPort, workerData } from 'node:worker_threads';import { fileURLToPath } from 'node:url';// Si estamos en el workerif (!isMainThread) { const resultado = calcularPrimos(workerData.limite); parentPort.postMessage(resultado);}// En el hilo principalexport function calcularEnWorker(limite) { return new Promise((resolve, reject) => { const worker = new Worker(fileURLToPath(import.meta.url), { workerData: { limite }, }); worker.on('message', resolve); worker.on('error', reject); });}function calcularPrimos(limite) { // Operación pesada de CPU que no bloquea el event loop const primos = []; for (let i = 2; i <= limite; i++) { if (esPrimo(i)) primos.push(i); } return primos;}
javascript
export class Cache { #store = new Map(); #ttls = new Map(); set(key, value, ttlMs = 60_000) { this.#store.set(key, value); const timeout = setTimeout(() => this.delete(key), ttlMs); timeout.unref(); // No impide que Node.js cierre this.#ttls.set(key, timeout); } get(key) { return this.#store.get(key); } delete(key) { clearTimeout(this.#ttls.get(key)); this.#ttls.delete(key); this.#store.delete(key); } has(key) { return this.#store.has(key); }}// Uso en el serviceconst cache = new Cache();async function obtenerProductos() { const KEY = 'productos:todos'; if (cache.has(KEY)) { return cache.get(KEY); } const productos = await repo.findAll(); cache.set(KEY, productos, 5 * 60_000); // 5 minutos return productos;}
javascript
// ✅ Usa # para encapsular estado internoclass ServicioAutenticacion { #secreto; #intentosFallidos = new Map(); constructor(secreto) { this.#secreto = secreto; // No accesible desde fuera } async verificarToken(token) { // ... }}
javascript
// En src/index.js — siempre añadir estos handlersprocess.on('uncaughtException', (error) => { logger.fatal({ error: error.message, stack: error.stack }, 'Error no capturado'); process.exit(1); // Siempre salir — el proceso está en estado incierto});process.on('unhandledRejection', (reason) => { logger.fatal({ reason }, 'Promise rechazada sin manejar'); process.exit(1);});
dockerfile
# Etapa 1: DependenciasFROM node:22-alpine AS depsWORKDIR /appCOPY package*.json ./RUN npm ci --only=production && npm cache clean --force# Etapa 2: Build (si usas TypeScript o similar)FROM node:22-alpine AS builderWORKDIR /appCOPY package*.json ./RUN npm ciCOPY . .RUN npm run build 2>/dev/null || true # No falla si no hay build# Etapa 3: Imagen final (mínima y segura)FROM node:22-alpine AS runnerWORKDIR /app# Usuario no-root para seguridadRUN addgroup -S appgroup && adduser -S appuser -G appgroupUSER appuserCOPY --from=deps --chown=appuser:appgroup /app/node_modules ./node_modulesCOPY --from=builder --chown=appuser:appgroup /app/src ./srcCOPY --chown=appuser:appgroup package.json ./ENV NODE_ENV=productionEXPOSE 3000# Usa dumb-init para manejar señales correctamenteENTRYPOINT ["node"]CMD ["src/index.js"]