InterviewHack.ai
Start free
Blog/Preguntas de entrevista de Node.js — 50 con código y respuestas

Preguntas de entrevista de Node.js — 50 con código y respuestas

16 de septiembre de 2026

nodejsjavascript

Artículo SEO de 50 preguntas de entrevista de Node.js

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 setTimeout y setInterval cuyo 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
js
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 loop

Por 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. 1process.nextTick — se ejecuta al final de la operación actual, antes de que el event loop avance a la siguiente fase
  2. 2Promise.then (microtasks) — se vacía la cola de microtasks completa después de cada tick
  3. 3setImmediate — se ejecuta en la fase check, después del poll de I/O
js
setImmediate(() => console.log('setImmediate'));
Promise.resolve().then(() => console.log('Promise'));
process.nextTick(() => console.log('nextTick'));

// Output:
// nextTick
// Promise
// setImmediate

Caso real: process.nextTick es útil para emitir eventos de forma asincrónica dentro de un constructor, sin bloquear al que llama:

js
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:

js
// 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:

js
// 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.

js
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:

js
// 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.

js
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í |

js
// 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:

js
// 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.

js
// 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.js

Esto 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:

js
// Forzar recarga en tests
delete require.cache[require.resolve('./modulo')];
const modulo = require('./modulo'); // instancia fresca

8. ¿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, constantes

La clave es que los services no conocen Express ni HTTP. Son testeables de forma unitaria sin levantar un servidor:

js
// 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:

js
// 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:

js
@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)
js
// 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:

js
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.

js
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.

js
// 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 usuario

HTTP, 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().

js
// 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:

js
// 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:

js
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:

js
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:

js
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.

js
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.

js
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:

js
// 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:

js
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?

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:

js
// 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?

js
// 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?

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:

js
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áticamente

25. ¿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:

js
// 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.

js
// 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?

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?

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?

js
// 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:

  1. 1Errores en tiempo de compilación, no en producción
  2. 2IntelliSense completo — autocompletado de propiedades de objetos
  3. 3Contratos explícitos entre capas — sabés exactamente qué entra y qué sale de cada función
  4. 4Refactoring seguro — el compilador encuentra todos los usos de lo que cambiás
ts
// 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?

ts
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
js
// 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):

js
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:

js
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 desarrollo

35. ¿Cómo configurás variables de entorno de forma segura?

js
// .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:

ts
// 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é?

js
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):

js
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// Output: 0, 1, 2

38. ¿Cómo implementarías una función `memoize`?

js
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 memoize

Para funciones async:

js
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?

js
// 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?

js
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?

js
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:

js
// 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:

bash
# 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 leak

43. ¿Cómo implementás WebSockets con Node.js?

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?

ts
// 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?

js
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:

js
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?

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)?

js
// 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?

js
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:

js
// 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.*

FAQ

¿Cuántas preguntas de Node.js caen en una entrevista técnica típica?+

Depende del rol y la empresa. Para un puesto backend o full-stack, esperá entre 5 y 15 preguntas técnicas de Node.js. Las más frecuentes cubren el event loop, async/await, manejo de errores y arquitectura de APIs. Las empresas que trabajan con alta carga también preguntan sobre streams, clustering y Redis.

¿Qué nivel de Node.js se espera para un rol junior vs. senior?+

Un junior debe dominar async/await, promesas, Express middleware, manejo básico de errores y estructura de proyectos. Un senior debe poder hablar de event loop en detalle, streams, backpressure, circuit breakers, observabilidad, diseño de microservicios y estrategias de deployment en producción.

¿Necesito saber TypeScript para una entrevista de Node.js en 2026?+

En la mayoría de las empresas tech que contratan remoto, sí. TypeScript es el estándar de facto para proyectos Node.js serios. No necesitás dominarlo al 100%, pero esperan que puedas tipar funciones, interfaces y respuestas de API, y que entiendas por qué se usa.

¿Cómo practico las preguntas de Node.js en voz alta antes de la entrevista?+

La mejor forma es practicar la respuesta hablada, no solo leerla. Tomá las preguntas más difíciles y explicá la respuesta en voz alta como si le hablaras al entrevistador. Si tartamujeás o perdés el hilo, el concepto no está fijado todavía. Herramientas como InterviewHack te dan feedback por pasada en tiempo real.

¿Qué diferencia a un candidato bueno de uno que consigue la oferta en una entrevista de Node.js?+

Los candidatos buenos saben las respuestas. Los que consiguen la oferta pueden conectar conceptos entre sí — por ejemplo, explicar cómo el event loop afecta la decisión de usar Worker Threads, o cómo el circuit breaker complementa el retry con backoff. También demuestran que entendieron los tradeoffs, no solo la solución.

¿Cuáles son los errores más comunes en entrevistas de Node.js?+

Los más frecuentes: 1) Decir que await bloquea el servidor (no bloquea el hilo, solo pausa esa función). 2) No mencionar el manejo de errores en código asíncrono. 3) Usar var dentro de loops con setTimeout sin entender el scope. 4) No saber la diferencia entre process.nextTick y setImmediate. 5) Proponer microservicios para proyectos pequeños sin considerar el overhead.

Related articles

Cómo usar el método STAR en entrevistas (con ejemplos reales)

Aprende a responder preguntas difíciles usando el método STAR en entrevistas. Consejos y ejemplos concretos para roles remotos tech de LATAM.

Cómo conseguir trabajo remoto en dólares desde LATAM: guía real

Descubre consejos concretos para conseguir trabajo remoto en dólares desde LATAM: estrategias de búsqueda, preparación y entrevista para roles tecnológicos.

Las mejores preguntas para hacerle al entrevistador al final

Descubre las mejores preguntas para hacerle al entrevistador al final, útiles para entrevistas tech remotas, diferenciándote y logrando roles en dólares.

Cómo preparar entrevistas de desarrollo sin experiencia previa

Consejos prácticos para enfrentar entrevistas de tu primer trabajo como desarrollador, incluso sin experiencia. Técnicas para destacar y convencer en cada etapa.

Prepare for your real interview

Paste your job link: we research who's interviewing you and rehearse you live.

Start free →

Have an interview coming up? Install the live copilot →

InterviewHack.ai

Prepare for the exact interview: who's interviewing you, a tailored CV, and a real coach.

Product

JobsFree ATS checkerInterview-English checkSalary checkLATAM salary reportFree coursesBlogTailored CVSpoken practiceIt's free

Remote jobs

ReactPythonFull-StackLATAMArgentinaMexicoSee all →

Prepare

Spoken practiceFrontendBackendAI EngineerBy companySell with your CV

Company

For employersAboutContactPrivacyTerms

© 2026 InterviewHack.ai · Your CV is yours. Never used to train anything. · A product of IA-PTY