Preguntas de entrevista de Node.js — 50 con código y respuestas
Si conseguiste una entrevista técnica para un rol de backend o full-stack con Node.js, esta guía es lo que necesitás leer antes. No son resúmenes de documentación. Son las preguntas que realmente caen, con respuestas explicadas y código que podés ejecutar.
Las organizamos de menor a mayor dificultad: empezamos con conceptos que cualquier senior espera que conozcas de memoria, y terminamos con arquitectura, rendimiento y casos límite que separan a los candidatos buenos de los que consiguen la oferta.
Fundamentos del event loop y la asincronía
1. ¿Qué es el event loop de Node.js y por qué importa?
Node.js corre en un solo hilo. Para no bloquearse esperando I/O (red, disco, base de datos), usa un modelo de event loop basado en libuv. Cuando hacés una operación asincrónica, Node la delega al sistema operativo o a un thread pool interno, libera el hilo principal, y cuando la operación termina, la callback se pone en cola para ejecutarse.
El event loop tiene fases ordenadas:
timers → pending callbacks → idle/prepare → poll → check → close callbacks- timers: ejecuta
setTimeoutysetIntervalcuyo delay ya venció - poll: espera nuevos eventos de I/O y ejecuta sus callbacks
- check: ejecuta callbacks de
setImmediate - close callbacks: limpieza de sockets cerrados abruptamente
console.log('1 - sync');
setTimeout(() => console.log('2 - setTimeout'), 0);
setImmediate(() => console.log('3 - setImmediate'));
Promise.resolve().then(() => console.log('4 - microtask'));
console.log('5 - sync');
// Output: 1, 5, 4, 2, 3
// Los microtasks (Promise) se ejecutan antes del siguiente tick del event loopPor qué importa en la entrevista: si bloqueás el hilo principal (loop síncrono pesado, JSON.parse de un archivo enorme), ningún request se atiende mientras tanto. Node no es mágicamente concurrente — es concurrente para I/O, no para CPU.
2. ¿Cuál es la diferencia entre `process.nextTick`, `Promise.then` y `setImmediate`?
Orden de ejecución:
- 1
process.nextTick— se ejecuta al final de la operación actual, antes de que el event loop avance a la siguiente fase - 2
Promise.then(microtasks) — se vacía la cola de microtasks completa después de cada tick - 3
setImmediate— se ejecuta en la fasecheck, después del poll de I/O
setImmediate(() => console.log('setImmediate'));
Promise.resolve().then(() => console.log('Promise'));
process.nextTick(() => console.log('nextTick'));
// Output:
// nextTick
// Promise
// setImmediateCaso real: process.nextTick es útil para emitir eventos de forma asincrónica dentro de un constructor, sin bloquear al que llama:
class MyEmitter extends EventEmitter {
constructor() {
super();
// Si hacés emit() acá directo, el listener aún no se registró
process.nextTick(() => this.emit('ready'));
}
}
const emitter = new MyEmitter();
emitter.on('ready', () => console.log('listo'));3. ¿Qué es el callback hell y cómo se resuelve?
Callback hell es el anidamiento profundo de callbacks que hace el código ilegible y difícil de manejar errores:
// Mal — callback hell
fs.readFile('config.json', (err, config) => {
if (err) return handleError(err);
db.connect(JSON.parse(config), (err, conn) => {
if (err) return handleError(err);
conn.query('SELECT * FROM users', (err, users) => {
if (err) return handleError(err);
// ... y sigue
});
});
});Soluciones en orden de modernidad:
// 1. Promesas
const config = await fs.promises.readFile('config.json', 'utf8');
const conn = await db.connect(JSON.parse(config));
const users = await conn.query('SELECT * FROM users');
// 2. async/await (sobre promesas — misma cosa, mejor sintaxis)
async function getUsers() {
try {
const config = await fs.promises.readFile('config.json', 'utf8');
const conn = await db.connect(JSON.parse(config));
return conn.query('SELECT * FROM users');
} catch (err) {
handleError(err);
}
}4. ¿Cómo funciona `async/await` bajo el capó?
async/await es azúcar sintáctica sobre promesas y generadores. Una función async siempre retorna una promesa. El await pausa la ejecución de esa función específica y devuelve el control al event loop — no bloquea el hilo.
async function fetchUser(id) {
const user = await db.findById(id); // el hilo sigue atendiendo otros requests mientras espera
return user;
}
// Es equivalente a:
function fetchUser(id) {
return db.findById(id).then(user => user);
}Error común en entrevistas: creer que await bloquea el servidor. No. Bloquea solo la ejecución de esa función. Otros requests se siguen procesando.
Error real de código:
// Mal — espera en secuencia, tarda 300ms
const user = await getUser(id); // 100ms
const orders = await getOrders(id); // 100ms
const prefs = await getPrefs(id); // 100ms
// Bien — paralelo, tarda ~100ms
const [user, orders, prefs] = await Promise.all([
getUser(id),
getOrders(id),
getPrefs(id),
]);5. ¿Qué hace `Promise.allSettled` y cuándo lo usás en vez de `Promise.all`?
Promise.all falla rápido: si cualquier promesa rechaza, el conjunto rechaza. Promise.allSettled espera todas y te dice el resultado de cada una.
const results = await Promise.allSettled([
fetch('/api/users'),
fetch('/api/orders'),
fetch('/api/broken-endpoint'), // esto va a fallar
]);
for (const result of results) {
if (result.status === 'fulfilled') {
console.log('ok:', result.value);
} else {
console.error('error:', result.reason);
}
}Usás Promise.allSettled cuando querés saber el estado de cada operación independientemente, por ejemplo al enviar notificaciones a múltiples usuarios: no querés que un fallo detenga todo el lote.
Módulos y arquitectura
6. ¿Cuál es la diferencia entre CommonJS y ES Modules?
CommonJS (require) es el sistema original de Node.js. ES Modules (import/export) es el estándar de JavaScript moderno, soportado en Node.js desde la versión 12.
| | CommonJS | ES Modules |
|---|---|---|
| Sintaxis | require() / module.exports | import / export |
| Carga | Síncrona | Asíncrona (pueden cargarse en paralelo) |
| Extensión | .js o .cjs | .mjs o .js con "type": "module" en package.json |
| __dirname | Disponible | No disponible (usar import.meta.url) |
| Top-level await | No | Sí |
// CommonJS
const express = require('express');
module.exports = { router };
// ES Modules
import express from 'express';
export { router };Gotcha real: no podés hacer require() de un ES Module desde CommonJS. Sí podés hacer import dinámico:
// Desde CommonJS, importar un ES Module
const { algo } = await import('./modulo.mjs');7. ¿Qué es el module caching y qué implicaciones tiene?
Node.js cachea los módulos la primera vez que se cargan. Cada llamada posterior a require('mismo-modulo') devuelve la misma instancia en memoria.
// counter.js
let count = 0;
module.exports = {
increment: () => ++count,
get: () => count,
};
// a.js
const counter = require('./counter');
counter.increment();
// b.js
const counter = require('./counter');
console.log(counter.get()); // 1 — misma instancia que a.jsEsto es útil para singletons (conexión a DB, configuración). También puede causar problemas en tests si no limpiás el caché entre pruebas:
// Forzar recarga en tests
delete require.cache[require.resolve('./modulo')];
const modulo = require('./modulo'); // instancia fresca8. ¿Cómo estructurías una aplicación Node.js grande?
Respuesta práctica que los entrevistadores buscan escuchar:
src/
routes/ # solo definición de rutas, delegan a controllers
controllers/ # lógica HTTP (req/res), llaman a services
services/ # lógica de negocio pura, sin HTTP
repositories/ # acceso a datos, abstraen la DB
middlewares/ # auth, validación, logging
models/ # schemas/types
utils/ # helpers sin estado
config/ # variables de entorno, constantesLa clave es que los services no conocen Express ni HTTP. Son testeables de forma unitaria sin levantar un servidor:
// service — pura lógica
async function createOrder(userId, items) {
const total = calculateTotal(items);
if (total <= 0) throw new Error('Total inválido');
return orderRepository.save({ userId, items, total });
}
// controller — solo HTTP
async function handleCreateOrder(req, res) {
try {
const order = await createOrder(req.user.id, req.body.items);
res.status(201).json(order);
} catch (err) {
res.status(400).json({ error: err.message });
}
}9. ¿Qué es el patrón Dependency Injection y por qué importa en Node.js?
En vez de que una función instancie sus dependencias directamente, las recibe como parámetros. Facilita el testing y el desacoplamiento:
// Sin DI — difícil de testear
async function getUser(id) {
const db = new Database(); // acoplado a esta implementación
return db.findById(id);
}
// Con DI — testeable
async function getUser(id, db) {
return db.findById(id);
}
// En tests:
const mockDb = { findById: jest.fn().mockResolvedValue({ id: 1 }) };
const user = await getUser(1, mockDb);Para apps grandes, se usan contenedores de DI como tsyringe o inversify:
@injectable()
class UserService {
constructor(@inject('IUserRepository') private repo: IUserRepository) {}
async getById(id: string) {
return this.repo.findById(id);
}
}Streams y Buffers
10. ¿Qué son los Streams en Node.js y cuándo los usás?
Un Stream es una abstracción para leer o escribir datos en secuencia, sin cargar todo en memoria. Hay cuatro tipos:
- Readable: fuente de datos (fs.createReadStream, HTTP request)
- Writable: destino (fs.createWriteStream, HTTP response)
- Duplex: lectura y escritura (TCP socket)
- Transform: duplex que modifica datos en tránsito (zlib.createGzip)
// Sin streams — carga 1GB completo en RAM
const data = fs.readFileSync('archivo-enorme.csv');
res.send(data);
// Con streams — usa memoria constante (unos KB de buffer)
const stream = fs.createReadStream('archivo-enorme.csv');
stream.pipe(res);Ejemplo real: comprimir un archivo al vuelo:
const { createReadStream, createWriteStream } = require('fs');
const { createGzip } = require('zlib');
const { pipeline } = require('stream/promises');
async function comprimir(entrada, salida) {
await pipeline(
createReadStream(entrada),
createGzip(),
createWriteStream(salida)
);
}Usá pipeline en vez de .pipe() porque maneja errores y cierra todos los streams correctamente.
11. ¿Qué es backpressure en streams y cómo lo manejás?
Backpressure ocurre cuando el productor genera datos más rápido de lo que el consumidor puede procesarlos. Si no lo manejás, la memoria crece sin límite.
const readable = fs.createReadStream('enorme.csv');
const writable = fs.createWriteStream('destino.csv');
// Mal — ignorar el retorno de write()
readable.on('data', (chunk) => {
writable.write(chunk); // puede llenarse el buffer interno
});
// Bien — respetar backpressure
readable.on('data', (chunk) => {
const ok = writable.write(chunk);
if (!ok) {
readable.pause(); // frena el productor
writable.once('drain', () => readable.resume()); // reanuda cuando el consumidor esté listo
}
});
// Mejor aún — pipeline lo hace automáticamente
pipeline(readable, writable).catch(console.error);12. ¿Qué es un Buffer y cómo se diferencia de un string?
Un Buffer es un array de bytes de tamaño fijo en memoria. Se usa para datos binarios (imágenes, audio, datos TCP) antes de convertirlos a un tipo de datos con un encoding.
// Crear buffers
const buf = Buffer.from('hola mundo', 'utf8');
console.log(buf); // <Buffer 68 6f 6c 61 20 6d 75 6e 64 6f>
console.log(buf.length); // 10 — bytes, no caracteres
// Convertir de vuelta
console.log(buf.toString('utf8')); // 'hola mundo'
console.log(buf.toString('base64')); // 'aG9sYSBtdW5kbw=='
// Allocar buffer vacío (seguro — llena con ceros)
const buf2 = Buffer.alloc(10);
// Gotcha: Buffer.allocUnsafe es más rápido pero puede tener datos de memoria previa
const buf3 = Buffer.allocUnsafe(10); // NO usar para datos que se expondrán al usuarioHTTP, Express y APIs
13. ¿Cómo funciona el middleware en Express?
Un middleware es una función con la firma (req, res, next). Puede modificar req/res, terminar el ciclo, o pasar al siguiente middleware llamando next().
// Middleware de logging
function logger(req, res, next) {
console.log(`${req.method} ${req.path}`);
next(); // sin esto, la request queda colgada
}
// Middleware de autenticación
function auth(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'No autorizado' });
try {
req.user = jwt.verify(token, process.env.JWT_SECRET);
next();
} catch {
res.status(401).json({ error: 'Token inválido' });
}
}
// Uso
app.use(logger); // global
app.get('/profile', auth, (req, res) => { // solo esta ruta
res.json(req.user);
});Error de manejo: siempre pasá los errores a next(err) y tené un error handler con 4 argumentos al final:
// Error handler
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(err.status || 500).json({ error: err.message });
});14. ¿Cómo implementás rate limiting en una API?
Rate limiting previene abuso y ataques de fuerza bruta. Con express-rate-limit:
import rateLimit from 'express-rate-limit';
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutos
max: 100, // máximo 100 requests por ventana
standardHeaders: true, // incluye headers RateLimit-* en respuesta
legacyHeaders: false,
handler: (req, res) => {
res.status(429).json({
error: 'Demasiadas solicitudes, intentá de nuevo más tarde',
});
},
});
app.use('/api/', limiter);Para rate limiting distribuido (múltiples instancias), usás Redis como store:
import RedisStore from 'rate-limit-redis';
import { createClient } from 'redis';
const client = createClient({ url: process.env.REDIS_URL });
await client.connect();
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
store: new RedisStore({ sendCommand: (...args) => client.sendCommand(args) }),
});15. ¿Cómo manejás la validación de datos de entrada?
Nunca confiés en lo que viene del cliente. Usá Zod o Joi para validar:
import { z } from 'zod';
const CreateUserSchema = z.object({
email: z.string().email(),
password: z.string().min(8).max(128),
age: z.number().int().min(18).optional(),
});
// Middleware de validación genérico
function validate(schema) {
return (req, res, next) => {
const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
error: 'Datos inválidos',
details: result.error.flatten().fieldErrors,
});
}
req.validated = result.data; // solo datos validados
next();
};
}
app.post('/users', validate(CreateUserSchema), async (req, res) => {
const user = await createUser(req.validated);
res.status(201).json(user);
});16. ¿Cómo implementás autenticación con JWT?
JWT (JSON Web Token) tiene tres partes: header, payload y firma. El servidor firma el token con un secreto; el cliente lo envía en cada request.
import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt';
// Login — genera el token
async function login(req, res) {
const { email, password } = req.body;
const user = await User.findByEmail(email);
if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
return res.status(401).json({ error: 'Credenciales inválidas' });
}
const token = jwt.sign(
{ sub: user.id, email: user.email },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
res.json({ token });
}
// Verificación — middleware
function requireAuth(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Token requerido' });
}
try {
const payload = jwt.verify(authHeader.slice(7), process.env.JWT_SECRET);
req.user = payload;
next();
} catch (err) {
res.status(401).json({ error: 'Token inválido o expirado' });
}
}Puntos que buscan los entrevistadores:
- Nunca almacenás el JWT en localStorage si hay riesgo de XSS (preferir httpOnly cookies)
- El secreto debe ser largo y aleatorio (
crypto.randomBytes(64).toString('hex')) - Para invalidar tokens antes de expirar, necesitás una blocklist en Redis
17. ¿Cómo manejás CORS en una API de Node.js?
CORS (Cross-Origin Resource Sharing) define qué orígenes pueden hacer requests a tu API desde el navegador.
import cors from 'cors';
// Simple — permite todos (solo para desarrollo)
app.use(cors());
// Producción — lista blanca
const allowedOrigins = [
'https://tuapp.com',
'https://www.tuapp.com',
process.env.NODE_ENV === 'development' && 'http://localhost:3000',
].filter(Boolean);
app.use(cors({
origin: (origin, callback) => {
// null = requests sin origen (mobile apps, curl)
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Origen no permitido por CORS'));
}
},
credentials: true, // permite cookies cross-origin
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization'],
}));Base de datos y rendimiento
18. ¿Cómo prevenís SQL injection en Node.js?
Nunca interpolás variables en queries SQL directamente. Usás queries parametrizadas:
// NUNCA hacer esto
const query = `SELECT * FROM users WHERE email = '${req.body.email}'`;
// Un atacante puede enviar: email = "' OR 1=1 --"
// Con node-postgres (pg) — parámetros posicionales
const { rows } = await pool.query(
'SELECT * FROM users WHERE email = $1 AND active = $2',
[req.body.email, true]
);
// Con MySQL2 — placeholders
const [rows] = await pool.execute(
'SELECT * FROM users WHERE email = ? AND active = ?',
[req.body.email, true]
);
// Con un ORM como Prisma — automáticamente seguro
const user = await prisma.user.findFirst({
where: { email: req.body.email, active: true },
});19. ¿Cómo manejás un connection pool a la base de datos?
Abrir una conexión nueva por cada query es muy caro. Un pool mantiene conexiones reutilizables:
import { Pool } from 'pg';
// Crear el pool UNA VEZ al iniciar la app (no en cada request)
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 20, // máximo de conexiones simultáneas
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
// Uso en queries
async function getUser(id) {
const { rows } = await pool.query('SELECT * FROM users WHERE id = $1', [id]);
return rows[0];
}
// Para transacciones — necesitás una conexión dedicada
async function transferMoney(fromId, toId, amount) {
const client = await pool.connect();
try {
await client.query('BEGIN');
await client.query('UPDATE accounts SET balance = balance - $1 WHERE id = $2', [amount, fromId]);
await client.query('UPDATE accounts SET balance = balance + $1 WHERE id = $2', [amount, toId]);
await client.query('COMMIT');
} catch (err) {
await client.query('ROLLBACK');
throw err;
} finally {
client.release(); // siempre devolver al pool
}
}20. ¿Cómo implementás caching con Redis en Node.js?
import { createClient } from 'redis';
const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();
// Patrón cache-aside
async function getUserById(id) {
const cacheKey = `user:${id}`;
// 1. Intentar desde caché
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached);
// 2. Si no está, ir a la DB
const user = await db.query('SELECT * FROM users WHERE id = $1', [id]);
if (!user) return null;
// 3. Guardar en caché con TTL
await redis.setEx(cacheKey, 3600, JSON.stringify(user)); // expira en 1 hora
return user;
}
// Invalidar caché al actualizar
async function updateUser(id, data) {
const user = await db.update('users', id, data);
await redis.del(`user:${id}`); // invalida caché
return user;
}21. ¿Cómo implementás paginación eficiente?
Offset pagination es simple pero lenta en tablas grandes. Cursor pagination es más eficiente:
// Offset — simple pero O(n) en tablas grandes
async function getUsersOffset(page, pageSize) {
const offset = (page - 1) * pageSize;
const { rows } = await pool.query(
'SELECT * FROM users ORDER BY id LIMIT $1 OFFSET $2',
[pageSize, offset]
);
const { rows: [{ count }] } = await pool.query('SELECT COUNT(*) FROM users');
return {
data: rows,
total: parseInt(count),
page,
totalPages: Math.ceil(count / pageSize),
};
}
// Cursor — eficiente para feeds infinitos
async function getUsersCursor(cursor, pageSize) {
const { rows } = await pool.query(
`SELECT * FROM users
WHERE id > $1
ORDER BY id
LIMIT $2`,
[cursor || 0, pageSize + 1] // pedir uno extra para saber si hay más
);
const hasMore = rows.length > pageSize;
const data = rows.slice(0, pageSize);
return {
data,
nextCursor: hasMore ? data[data.length - 1].id : null,
};
}Manejo de errores
22. ¿Cuál es la diferencia entre errores síncronos y asíncronos y cómo los manejás?
// Errores síncronos — capturables con try/catch
function parseConfig(json) {
try {
return JSON.parse(json);
} catch (err) {
throw new Error(`Config inválida: ${err.message}`);
}
}
// Errores en promesas — necesitan .catch() o try/catch con await
async function loadUser(id) {
try {
const user = await db.findById(id);
if (!user) throw new NotFoundError(`Usuario ${id} no existe`);
return user;
} catch (err) {
if (err instanceof NotFoundError) throw err;
throw new DatabaseError('Error al leer usuario', { cause: err });
}
}
// Errores no capturados — SIEMPRE manejar
process.on('unhandledRejection', (reason, promise) => {
console.error('Promesa no manejada:', reason);
// Idealmente loggear y hacer graceful shutdown
process.exit(1);
});
process.on('uncaughtException', (err) => {
console.error('Excepción no capturada:', err);
process.exit(1); // el proceso puede estar en estado inconsistente
});23. ¿Cómo creás errores personalizados en Node.js?
class AppError extends Error {
constructor(message, statusCode = 500, code = 'INTERNAL_ERROR') {
super(message);
this.name = this.constructor.name;
this.statusCode = statusCode;
this.code = code;
Error.captureStackTrace(this, this.constructor);
}
}
class NotFoundError extends AppError {
constructor(resource) {
super(`${resource} no encontrado`, 404, 'NOT_FOUND');
}
}
class ValidationError extends AppError {
constructor(message, fields) {
super(message, 400, 'VALIDATION_ERROR');
this.fields = fields;
}
}
// Error handler de Express que los aprovecha
app.use((err, req, res, next) => {
if (err instanceof AppError) {
return res.status(err.statusCode).json({
error: { code: err.code, message: err.message, ...(err.fields && { fields: err.fields }) },
});
}
// Error no esperado
console.error(err);
res.status(500).json({ error: { code: 'INTERNAL_ERROR', message: 'Error interno' } });
});Seguridad
24. ¿Qué headers de seguridad debería tener una API Node.js?
Usás helmet para los más importantes:
import helmet from 'helmet';
app.use(helmet()); // activa todos los defaults
// Equivale a configurar manualmente:
app.use(helmet.contentSecurityPolicy()); // previene XSS
app.use(helmet.hsts()); // fuerza HTTPS
app.use(helmet.noSniff()); // previene MIME sniffing
app.use(helmet.frameguard({ action: 'deny' })); // previene clickjacking
app.use(helmet.xssFilter()); // filtro XSS del navegador
// También removés headers que revelan info:
app.disable('x-powered-by'); // o helmet lo hace automáticamente25. ¿Cómo prevenís ataques de mass assignment?
Mass assignment ocurre cuando aceptás todos los campos del body y los guardás en la DB, incluyendo campos sensibles como role o isAdmin:
// Vulnerable
app.put('/users/:id', async (req, res) => {
const user = await User.update(req.params.id, req.body); // puede actualizar req.body.isAdmin = true
res.json(user);
});
// Seguro — lista blanca de campos permitidos
app.put('/users/:id', async (req, res) => {
const allowedFields = ['name', 'bio', 'avatar'];
const updates = Object.fromEntries(
Object.entries(req.body).filter(([key]) => allowedFields.includes(key))
);
const user = await User.update(req.params.id, updates);
res.json(user);
});Worker Threads y rendimiento
26. ¿Cuándo usás Worker Threads vs. clustering?
Worker Threads son para operaciones CPU-intensivas dentro del mismo proceso. Comparten memoria a través de SharedArrayBuffer.
Cluster crea múltiples procesos Node.js que comparten el mismo puerto, aprovechando todos los núcleos de la CPU para I/O concurrente.
// Worker Thread — tarea CPU pesada
const { Worker, isMainThread, parentPort } = require('worker_threads');
if (isMainThread) {
const worker = new Worker(__filename);
worker.on('message', (result) => console.log('Resultado:', result));
worker.postMessage({ data: arrayEnorme });
} else {
parentPort.on('message', ({ data }) => {
const result = procesarPesado(data); // no bloquea el hilo principal
parentPort.postMessage(result);
});
}
// Cluster — múltiples instancias del servidor
import cluster from 'cluster';
import os from 'os';
if (cluster.isPrimary) {
const cpus = os.cpus().length;
console.log(`Creando ${cpus} workers`);
for (let i = 0; i < cpus; i++) cluster.fork();
cluster.on('exit', (worker) => {
console.log(`Worker ${worker.id} murió, reemplazando`);
cluster.fork();
});
} else {
// Cada worker corre el servidor
const app = createApp();
app.listen(3000);
}27. ¿Cómo perfilás y encontrás cuellos de botella en una app Node.js?
// 1. Node.js built-in profiler
node --prof app.js
node --prof-process isolate-*.log > profile.txt
// 2. Clinic.js — herramienta visual
npx clinic doctor -- node app.js
npx clinic flame -- node app.js # flamegraph de CPU
npx clinic bubbleprof -- node app.js # análisis de async
// 3. console.time para medición puntual
console.time('operacion-costosa');
await operacionCostosa();
console.timeEnd('operacion-costosa');
// 4. Performance hooks para medición en producción
import { performance, PerformanceObserver } from 'perf_hooks';
const obs = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(`${entry.name}: ${entry.duration}ms`);
}
});
obs.observe({ type: 'measure' });
performance.mark('inicio');
await operacionCostosa();
performance.mark('fin');
performance.measure('operación', 'inicio', 'fin');Testing
28. ¿Cómo escribís tests unitarios para un servicio de Node.js?
// user.service.test.js
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { UserService } from './user.service';
describe('UserService', () => {
let service;
let mockRepo;
beforeEach(() => {
mockRepo = {
findById: vi.fn(),
save: vi.fn(),
};
service = new UserService(mockRepo);
});
it('debería devolver un usuario cuando existe', async () => {
const fakeUser = { id: '1', name: 'Ana', email: 'ana@test.com' };
mockRepo.findById.mockResolvedValue(fakeUser);
const result = await service.getById('1');
expect(result).toEqual(fakeUser);
expect(mockRepo.findById).toHaveBeenCalledWith('1');
});
it('debería lanzar NotFoundError si el usuario no existe', async () => {
mockRepo.findById.mockResolvedValue(null);
await expect(service.getById('999')).rejects.toThrow('Usuario 999 no encontrado');
});
});29. ¿Cómo hacés tests de integración de una API REST?
// Con supertest + vitest
import request from 'supertest';
import { app } from '../app';
import { pool } from '../db';
describe('POST /api/users', () => {
afterEach(async () => {
await pool.query('DELETE FROM users WHERE email LIKE $1', ['%test%']);
});
it('crea un usuario con datos válidos', async () => {
const response = await request(app)
.post('/api/users')
.send({ name: 'Test User', email: 'test@test.com', password: 'segura123' })
.expect(201);
expect(response.body).toMatchObject({
id: expect.any(String),
name: 'Test User',
email: 'test@test.com',
});
expect(response.body.password).toBeUndefined(); // nunca en la respuesta
});
it('rechaza emails duplicados con 409', async () => {
await request(app)
.post('/api/users')
.send({ name: 'User 1', email: 'dup@test.com', password: 'pass123' });
const response = await request(app)
.post('/api/users')
.send({ name: 'User 2', email: 'dup@test.com', password: 'pass456' })
.expect(409);
expect(response.body.error.code).toBe('EMAIL_TAKEN');
});
});TypeScript con Node.js
30. ¿Por qué usarías TypeScript en un proyecto Node.js?
TypeScript agrega tipado estático al JavaScript. Los beneficios concretos para APIs:
- 1Errores en tiempo de compilación, no en producción
- 2IntelliSense completo — autocompletado de propiedades de objetos
- 3Contratos explícitos entre capas — sabés exactamente qué entra y qué sale de cada función
- 4Refactoring seguro — el compilador encuentra todos los usos de lo que cambiás
// Con TypeScript — el error se detecta antes de deployar
interface CreateUserDto {
name: string;
email: string;
password: string;
}
interface User {
id: string;
name: string;
email: string;
createdAt: Date;
}
async function createUser(dto: CreateUserDto): Promise<User> {
// El compilador garantiza que dto tiene los campos correctos
// y que el retorno es User (sin el campo password)
const hash = await bcrypt.hash(dto.password, 10);
return db.users.create({ ...dto, password: undefined, passwordHash: hash });
}31. ¿Cómo tipás req/res de Express con TypeScript?
import { Request, Response, NextFunction } from 'express';
// Extender Request para agregar campos personalizados
interface AuthenticatedRequest extends Request {
user: {
id: string;
email: string;
role: 'admin' | 'user';
};
}
// Con generics para body, params y query tipados
type CreatePostBody = { title: string; content: string };
type PostParams = { id: string };
type SearchQuery = { q?: string; page?: string };
app.post(
'/posts',
requireAuth,
async (
req: Request<PostParams, unknown, CreatePostBody, SearchQuery>,
res: Response
) => {
const { title, content } = req.body; // tipado, con autocompletado
// ...
}
);Arquitectura avanzada
32. ¿Qué es un microservicio y cuándo tiene sentido en Node.js?
Un microservicio es un servicio independiente con responsabilidad única, su propio proceso, despliegue y (a veces) base de datos.
Tiene sentido cuando:
- Equipos grandes, donde múltiples grupos trabajan en partes distintas
- Partes con diferentes requisitos de escala (el servicio de imágenes necesita más CPU que el de autenticación)
- Necesitás deployar partes de forma independiente
No tiene sentido cuando:
- Startup con 2 devs (el overhead es enorme: service discovery, observabilidad distribuida, mensajería, transacciones distribuidas)
- No tenés aún claro los límites de dominio
// Comunicación entre servicios — REST o mensajería
// Con Redis Pub/Sub para eventos:
import { createClient } from 'redis';
const publisher = createClient();
const subscriber = createClient();
// Servicio de órdenes publica el evento
await publisher.publish('order.created', JSON.stringify({ orderId: '123', userId: '456' }));
// Servicio de emails se suscribe
await subscriber.subscribe('order.created', (message) => {
const { userId, orderId } = JSON.parse(message);
emailService.sendOrderConfirmation(userId, orderId);
});33. ¿Cómo implementás una cola de trabajos en Node.js?
Para tareas que no deben bloquear el request (envío de emails, procesamiento de imágenes, reportes pesados):
import { Queue, Worker } from 'bullmq';
import { createClient } from 'redis';
const connection = createClient({ url: process.env.REDIS_URL });
// Crear la cola
const emailQueue = new Queue('emails', { connection });
// En el controller — agregar trabajo a la cola
app.post('/users', async (req, res) => {
const user = await createUser(req.body);
// No esperar el email — solo encolarlo
await emailQueue.add('welcome-email', {
to: user.email,
name: user.name,
});
res.status(201).json(user); // responde inmediatamente
});
// En un worker separado — procesar la cola
const worker = new Worker('emails', async (job) => {
await emailService.send({
to: job.data.to,
subject: `Bienvenido, ${job.data.name}`,
template: 'welcome',
});
}, { connection, concurrency: 5 });
worker.on('failed', (job, err) => {
console.error(`Email fallido para ${job.data.to}:`, err);
// BullMQ reintenta automáticamente según la config
});34. ¿Cómo hacés graceful shutdown de un servidor Node.js?
Un shutdown incorrecto corta conexiones activas y puede corromper transacciones. El graceful shutdown espera a que terminen los requests en curso:
const server = app.listen(3000);
async function shutdown(signal) {
console.log(`Recibido ${signal}, apagando gracefully...`);
// 1. Dejar de aceptar nuevas conexiones
server.close(async () => {
try {
// 2. Cerrar conexiones a recursos
await pool.end(); // cerrar pool de DB
await redis.disconnect(); // cerrar Redis
console.log('Apagado limpio completado');
process.exit(0);
} catch (err) {
console.error('Error en shutdown:', err);
process.exit(1);
}
});
// 3. Forzar apagado si tarda demasiado (requests colgados)
setTimeout(() => {
console.error('Shutdown forzado por timeout');
process.exit(1);
}, 30000);
}
process.on('SIGTERM', () => shutdown('SIGTERM')); // signal de Kubernetes/Docker
process.on('SIGINT', () => shutdown('SIGINT')); // Ctrl+C en desarrollo35. ¿Cómo configurás variables de entorno de forma segura?
// .env (nunca comiteado al repo)
DATABASE_URL=postgres://user:password@localhost/mydb
JWT_SECRET=supersecret_al_menos_64_caracteres_random
NODE_ENV=development
// Con dotenv + validación (en el entry point, antes de todo)
import { config } from 'dotenv';
import { z } from 'zod';
config(); // carga .env
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),
REDIS_URL: z.string().url().optional(),
});
const parsed = EnvSchema.safeParse(process.env);
if (!parsed.success) {
console.error('Variables de entorno inválidas:');
console.error(parsed.error.flatten().fieldErrors);
process.exit(1); // falla rápido si el entorno no está configurado correctamente
}
export const env = parsed.data;36. ¿Qué es el patrón Repository y cómo lo implementás?
El Repository abstrae el acceso a datos. El resto de la app no sabe si usás Postgres, MongoDB o un archivo en disco:
// Interfaz
interface IUserRepository {
findById(id: string): Promise<User | null>;
findByEmail(email: string): Promise<User | null>;
save(user: Omit<User, 'id' | 'createdAt'>): Promise<User>;
update(id: string, data: Partial<User>): Promise<User>;
delete(id: string): Promise<void>;
}
// Implementación con Postgres
class PostgresUserRepository implements IUserRepository {
async findById(id: string) {
const { rows } = await pool.query('SELECT * FROM users WHERE id = $1', [id]);
return rows[0] ?? null;
}
// ...
}
// Implementación en memoria para tests
class InMemoryUserRepository implements IUserRepository {
private users = new Map<string, User>();
async findById(id: string) {
return this.users.get(id) ?? null;
}
async save(data: Omit<User, 'id' | 'createdAt'>) {
const user = { ...data, id: crypto.randomUUID(), createdAt: new Date() };
this.users.set(user.id, user);
return user;
}
// ...
}Preguntas de código en vivo
37. ¿Cuál es el output de este código? ¿Por qué?
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}Output: 3, 3, 3
Explicación: var es function-scoped, no block-scoped. Los tres setTimeout comparten la misma variable i. Para cuando el event loop ejecuta los callbacks, el for ya terminó y i es 3.
Fix con let (block-scoped):
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// Output: 0, 1, 238. ¿Cómo implementarías una función `memoize`?
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
// Uso
const fibonacci = memoize(function fib(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
});
console.log(fibonacci(40)); // rápido gracias al memoizePara funciones async:
function memoizeAsync(fn) {
const cache = new Map();
return async function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const promise = fn.apply(this, args);
cache.set(key, promise); // cachear la promesa, no el resultado
return promise;
};
}39. ¿Cómo implementarías un debounce y un throttle?
// Debounce — ejecuta la función DESPUÉS de que paren los llamados por `delay` ms
function debounce(fn, delay) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
// Throttle — ejecuta la función A LO SUMO una vez cada `limit` ms
function throttle(fn, limit) {
let lastCall = 0;
return function(...args) {
const now = Date.now();
if (now - lastCall >= limit) {
lastCall = now;
return fn.apply(this, args);
}
};
}
// Diferencia clave:
// - debounce: útil para búsqueda en tiempo real (esperar que el usuario deje de tipear)
// - throttle: útil para scroll/resize (limitar la frecuencia de ejecución)
const handleSearch = debounce((query) => fetchResults(query), 300);
const handleScroll = throttle(() => updateUI(), 100);40. ¿Cómo implementarías un EventEmitter básico?
class SimpleEventEmitter {
constructor() {
this.events = {};
}
on(event, listener) {
if (!this.events[event]) this.events[event] = [];
this.events[event].push(listener);
return this; // permite chaining
}
off(event, listener) {
this.events[event] = (this.events[event] || []).filter(l => l !== listener);
return this;
}
once(event, listener) {
const wrapper = (...args) => {
listener(...args);
this.off(event, wrapper);
};
return this.on(event, wrapper);
}
emit(event, ...args) {
(this.events[event] || []).forEach(listener => listener(...args));
return this;
}
}
// Uso
const bus = new SimpleEventEmitter();
bus.on('data', (msg) => console.log('recibido:', msg));
bus.emit('data', 'hola'); // 'recibido: hola'41. ¿Cómo implementarías un retry con backoff exponencial?
async function retry(fn, options = {}) {
const {
maxAttempts = 3,
baseDelay = 1000,
maxDelay = 30000,
factor = 2,
shouldRetry = (err) => true,
} = options;
let attempt = 0;
while (attempt < maxAttempts) {
try {
return await fn();
} catch (err) {
attempt++;
if (attempt >= maxAttempts || !shouldRetry(err)) {
throw err;
}
const delay = Math.min(
baseDelay * Math.pow(factor, attempt - 1) + Math.random() * 1000, // jitter
maxDelay
);
console.log(`Intento ${attempt} fallido, reintentando en ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// Uso
const data = await retry(
() => fetch('https://api.externa.com/datos'),
{
maxAttempts: 4,
shouldRetry: (err) => err.status !== 404, // no reintentar 404
}
);42. ¿Qué es una "memory leak" en Node.js y cómo la detectás?
Un memory leak es cuando el heap crece continuamente porque hay referencias a objetos que nunca se liberan.
Causas comunes:
// 1. Closures que retienen objetos grandes
const cache = {};
app.get('/data/:id', (req, res) => {
cache[req.params.id] = { data: enormeObjeto, timestamp: Date.now() };
// cache nunca se limpia → crece indefinidamente
});
// 2. EventListeners no removidos
function setup() {
const handler = (data) => processData(data);
someEmitter.on('data', handler);
// si setup() se llama muchas veces, los listeners se apilan
// solución: someEmitter.off('data', handler) o .once()
}
// 3. Intervalos no limpiados
function startPolling() {
setInterval(() => checkDatabase(), 5000);
// si no guardás el id y no hacés clearInterval, sigue corriendo aunque no haga falta
}Detección:
# Monitorear heap en tiempo real
node --inspect app.js
# Abrí chrome://inspect en Chrome → Memory → Take Heap Snapshot
# Con clinic
npx clinic doctor -- node app.js
# Si el heap crece sin bajar, hay una leak43. ¿Cómo implementás WebSockets con Node.js?
import { WebSocketServer } from 'ws';
import { createServer } from 'http';
const httpServer = createServer(app);
const wss = new WebSocketServer({ server: httpServer });
// Map para manejar múltiples clientes
const clients = new Map(); // userId → ws
wss.on('connection', (ws, req) => {
const userId = extractUserIdFromRequest(req);
clients.set(userId, ws);
ws.on('message', (data) => {
const message = JSON.parse(data.toString());
handleMessage(userId, message, ws);
});
ws.on('close', () => {
clients.delete(userId);
});
ws.on('error', (err) => {
console.error(`Error WS usuario ${userId}:`, err);
clients.delete(userId);
});
// Ping/pong para detectar clientes muertos
ws.isAlive = true;
ws.on('pong', () => { ws.isAlive = true; });
});
// Heartbeat cada 30 segundos
const interval = setInterval(() => {
for (const [userId, ws] of clients) {
if (!ws.isAlive) {
clients.delete(userId);
ws.terminate();
continue;
}
ws.isAlive = false;
ws.ping();
}
}, 30000);
// Broadcast a todos los clientes
function broadcast(message) {
const data = JSON.stringify(message);
for (const ws of clients.values()) {
if (ws.readyState === ws.OPEN) {
ws.send(data);
}
}
}
httpServer.listen(3000);Preguntas avanzadas de arquitectura
44. ¿Cómo diseñarías el sistema de autenticación de una app con múltiples roles?
// RBAC (Role-Based Access Control)
type Role = 'admin' | 'editor' | 'viewer';
type Permission = 'users:read' | 'users:write' | 'posts:read' | 'posts:write' | 'posts:delete';
const ROLE_PERMISSIONS: Record<Role, Permission[]> = {
admin: ['users:read', 'users:write', 'posts:read', 'posts:write', 'posts:delete'],
editor: ['posts:read', 'posts:write'],
viewer: ['posts:read'],
};
function can(user: { role: Role }, permission: Permission): boolean {
return ROLE_PERMISSIONS[user.role]?.includes(permission) ?? false;
}
// Middleware de autorización
function requirePermission(permission: Permission) {
return (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
if (!can(req.user, permission)) {
return res.status(403).json({ error: 'Sin permisos suficientes' });
}
next();
};
}
// Uso
app.delete(
'/posts/:id',
requireAuth,
requirePermission('posts:delete'),
deletePostHandler
);45. ¿Cómo implementás circuit breaker para llamadas a servicios externos?
class CircuitBreaker {
constructor(fn, options = {}) {
this.fn = fn;
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
this.failureCount = 0;
this.successCount = 0;
this.lastFailureTime = null;
this.options = {
failureThreshold: 5,
successThreshold: 2,
timeout: 60000, // ms antes de pasar a HALF_OPEN
...options,
};
}
async call(...args) {
if (this.state === 'OPEN') {
if (Date.now() - this.lastFailureTime < this.options.timeout) {
throw new Error('Circuit breaker abierto — servicio no disponible');
}
this.state = 'HALF_OPEN';
}
try {
const result = await this.fn(...args);
this.onSuccess();
return result;
} catch (err) {
this.onFailure();
throw err;
}
}
onSuccess() {
this.failureCount = 0;
if (this.state === 'HALF_OPEN') {
this.successCount++;
if (this.successCount >= this.options.successThreshold) {
this.state = 'CLOSED';
this.successCount = 0;
}
}
}
onFailure() {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.options.failureThreshold) {
this.state = 'OPEN';
}
}
}
const breaker = new CircuitBreaker(callExternalAPI, { failureThreshold: 3 });
const data = await breaker.call('/endpoint');46. ¿Cómo garantizás que un cron job no se ejecute dos veces en paralelo (en múltiples instancias)?
Usás distributed locking con Redis:
import { createClient } from 'redis';
const redis = createClient({ url: process.env.REDIS_URL });
async function withLock(lockKey, ttlMs, fn) {
const lockId = crypto.randomUUID();
// SET NX EX es atómico — solo un proceso gana
const acquired = await redis.set(lockKey, lockId, {
NX: true, // solo si no existe
PX: ttlMs, // expira automáticamente
});
if (!acquired) {
console.log(`Lock ${lockKey} ya tomado, saltando`);
return;
}
try {
await fn();
} finally {
// Liberar solo si somos nosotros los que lo tenemos (con script Lua atómico)
await redis.eval(
`if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end`,
{ keys: [lockKey], arguments: [lockId] }
);
}
}
// Cron de limpieza diaria — seguro con múltiples instancias
cron.schedule('0 2 * * *', async () => {
await withLock('cron:daily-cleanup', 10 * 60 * 1000, async () => {
await cleanOldSessions();
await cleanExpiredTokens();
});
});47. ¿Cómo implementás observabilidad (logs, métricas, traces) en una API Node.js?
// 1. Logging estructurado con Pino
import pino from 'pino';
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
formatters: {
level: (label) => ({ level: label }),
},
});
// Middleware que loggea cada request
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
logger.info({
method: req.method,
url: req.url,
statusCode: res.statusCode,
durationMs: Date.now() - start,
userId: req.user?.id,
});
});
next();
});
// 2. Métricas con Prometheus
import promClient from 'prom-client';
const httpDuration = new promClient.Histogram({
name: 'http_request_duration_ms',
help: 'Duración de HTTP requests',
labelNames: ['method', 'route', 'status_code'],
buckets: [10, 50, 100, 200, 500, 1000],
});
// 3. Distributed tracing con OpenTelemetry
import { trace } from '@opentelemetry/api';
const tracer = trace.getTracer('mi-app');
async function getUserWithOrders(userId) {
const span = tracer.startSpan('getUserWithOrders');
try {
const [user, orders] = await Promise.all([
getUser(userId),
getOrders(userId),
]);
span.setAttributes({ userId, orderCount: orders.length });
return { user, orders };
} catch (err) {
span.recordException(err);
throw err;
} finally {
span.end();
}
}48. ¿Cómo manejás configuración para múltiples entornos (dev/staging/prod)?
// config/index.ts
import { env } from './env'; // validado con Zod (ver pregunta 35)
const configs = {
development: {
db: { pool: { max: 5 } },
cache: { ttl: 60 },
log: { level: 'debug' },
cors: { origins: ['http://localhost:3000'] },
},
test: {
db: { pool: { max: 2 } },
cache: { ttl: 0 }, // sin caché en tests
log: { level: 'silent' },
cors: { origins: ['*'] },
},
production: {
db: { pool: { max: 20 } },
cache: { ttl: 3600 },
log: { level: 'warn' },
cors: { origins: [process.env.FRONTEND_URL!] },
},
};
export const config = {
...configs[env.NODE_ENV],
port: env.PORT,
db: { url: env.DATABASE_URL, ...configs[env.NODE_ENV].db },
jwt: { secret: env.JWT_SECRET },
};49. ¿Cómo diseñarías una API que soporte uploads de archivos grandes?
import { createWriteStream } from 'fs';
import { pipeline } from 'stream/promises';
import multiparty from 'multiparty';
// Para archivos grandes: streaming directo al disco o a S3
app.post('/upload', requireAuth, (req, res) => {
const form = new multiparty.Form({ maxFilesSize: 100 * 1024 * 1024 }); // 100MB
form.on('part', async (part) => {
if (!part.filename) return part.resume();
// Validar tipo MIME
const allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'];
if (!allowedTypes.includes(part.headers['content-type'])) {
return res.status(400).json({ error: 'Tipo de archivo no permitido' });
}
// Stream directo a S3 sin pasar por disco
const key = `uploads/${req.user.id}/${Date.now()}-${part.filename}`;
const upload = s3.createMultipartUpload({ Bucket: 'mi-bucket', Key: key });
try {
const url = await uploadStreamToS3(part, key);
res.json({ url });
} catch (err) {
res.status(500).json({ error: 'Error al subir archivo' });
}
});
form.parse(req);
});
// Para archivos muy grandes: URLs pre-firmadas (el cliente sube directo a S3)
app.post('/upload/presigned', requireAuth, async (req, res) => {
const { filename, contentType } = req.body;
const key = `uploads/${req.user.id}/${Date.now()}-${filename}`;
const uploadUrl = await s3.getSignedUrlPromise('putObject', {
Bucket: 'mi-bucket',
Key: key,
ContentType: contentType,
Expires: 300, // 5 minutos
});
res.json({ uploadUrl, key });
});50. ¿Cómo preparás una app Node.js para producción?
Lista de verificación que los entrevistadores esperan que menciones:
// 1. Variables de entorno validadas al inicio (pregunta 35)
// 2. Graceful shutdown (pregunta 34)
// 3. Health check endpoint
app.get('/health', async (req, res) => {
try {
await pool.query('SELECT 1');
await redis.ping();
res.json({ status: 'ok', uptime: process.uptime() });
} catch {
res.status(503).json({ status: 'degraded' });
}
});
// 4. Compression
import compression from 'compression';
app.use(compression());
// 5. Manejo de señales OS (pregunta 34)
// 6. Límites de memoria
node --max-old-space-size=512 app.js
// 7. PM2 o similar para reinicio automático
// ecosystem.config.js
module.exports = {
apps: [{
name: 'api',
script: 'dist/index.js',
instances: 'max', // un proceso por CPU
exec_mode: 'cluster',
max_memory_restart: '512M',
env_production: {
NODE_ENV: 'production',
},
}],
};
// 8. Dockerfile optimizado para producción
// FROM node:20-alpine AS builder
// COPY package*.json ./
// RUN npm ci --only=production
// COPY . .
// RUN npm run build
//
// FROM node:20-alpine
// COPY --from=builder /app/dist ./dist
// COPY --from=builder /app/node_modules ./node_modules
// USER node # no correr como root
// CMD ["node", "dist/index.js"]Cómo practicar estas preguntas antes de tu entrevista
Leer respuestas no es suficiente. Los entrevistadores de Node.js buscan que puedas explicar con tus palabras, no recitar de memoria.
El problema real no es que no sepas las respuestas — es que bajo presión, cuando el entrevistador te mira y te pregunta "¿y qué pasa con el backpressure?", tu cerebro no encuentra el chunk correcto. Hay que entrenarlo a recuperar esos conceptos en voz alta.
Probá esto: tomá las preguntas que más te costaron de esta lista y decí la respuesta en voz alta, sin mirar. Si no podés explicarla fluidamente, todavía no está fijada.
InterviewHack te permite hacer exactamente eso — practicar en voz alta con feedback por pasada, contra las preguntas del rol específico que estás buscando. Y podés hacerlo desde el navegador o por WhatsApp. Es gratis.
[Empezar a practicar →](https://interviewhack.ai)
*¿Tenés una entrevista de Node.js pronto? Cargá la descripción del puesto en InterviewHack y generá un dossier con las preguntas específicas de esa empresa — basadas en tu CV y lo que realmente toman.*