Backend
Backend - Preguntas Comunes en Screening
Preguntas frecuentes que aparecen en procesos de screening técnico para posiciones de backend developer
ScreeningPreguntas ComunesEntrevistas
Introducción
Estas son algunas de las preguntas más comunes que encontrarás en procesos de screening técnico para posiciones de backend. Es importante tener claros estos conceptos fundamentales.
setTimeout, setImmediate, process.nextTick y Promises/async–await
1 preguntas
1
¿Cuál es la diferencia entre setTimeout, setImmediate, process.nextTick y Promises/async–await?
Orden de ejecución: Sync → process.nextTick → Promises → setImmediate → setTimeout
Comparación:
| Método | Prioridad | Cuándo se ejecuta |
|---|---|---|
process.nextTick | ⭐⭐⭐⭐⭐ Más alta | ANTES de la siguiente fase del event loop |
Promises/async-await | ⭐⭐⭐⭐ Alta | Microtask queue (después de nextTick) |
setImmediate | ⭐⭐ Media | Fase 'check' del event loop (después de I/O) |
setTimeout | ⭐ Baja | Fase 'timers' (puede tener delay) |
Ejemplo:
javascript
console.log('1: Sync');
setTimeout(() => console.log('2: setTimeout'), 0);
setImmediate(() => console.log('3: setImmediate'));
Promise.resolve().then(() => console.log('4: Promise'));
process.nextTick(() => console.log('5: nextTick'));
console.log('6: Sync');
// Salida: 1, 6, 5, 4, 3 (o 2), 2 (o 3)Cuándo usar:
•process.nextTick: Garantizar ejecución antes del event loop (cuidado con starvation)
•setImmediate: Preferible para código después de I/O
•Promises: Operaciones asíncronas modernas
•setTimeout: Operaciones con delay específico
Event Loop y Concurrencia
2 preguntas
1
¿Cómo funciona el event loop?
Concepto clave: Node.js es single-threaded pero maneja operaciones asíncronas eficientemente.
Fases del Event Loop:
1.Timers: Ejecuta callbacks de setTimeout y setInterval
2.Pending Callbacks: Ejecuta callbacks diferidos de I/O
3.Idle, Prepare: Uso interno
4.Poll: Obtiene nuevos eventos I/O
5.Check: Ejecuta callbacks de setImmediate
6.Close Callbacks: Ejecuta callbacks de close (ej: socket.on('close'))
Flujo:
┌───────────────────────────┐
│ Timers │ ← setTimeout, setInterval
├───────────────────────────┤
│ Pending Callbacks │ ← I/O callbacks diferidos
├───────────────────────────┤
│ Poll │ ← Fetch nuevos eventos I/O
├───────────────────────────┤
│ Check │ ← setImmediate callbacks
├───────────────────────────┤
│ Close Callbacks │ ← socket.on('close')
└───────────────────────────┘Microtasks (mayor prioridad):
•process.nextTick
•Promises/async-await
Características:
✓Single-threaded: Un solo hilo ejecuta JavaScript
✓Non-blocking: I/O asíncrono no bloquea
✓Event-driven: Responde a eventos
Ejemplo:
javascript
console.log('Start');
setTimeout(() => console.log('Timer'), 0);
Promise.resolve().then(() => console.log('Promise'));
console.log('End');
// Salida: Start, End, Promise, Timer2
¿Qué pasa si bloqueas el thread principal?
Problema: Node.js es single-threaded, si bloqueas el thread principal, toda la aplicación se congela.
Qué bloquea el thread:
✗ Loops síncronos largos:
javascript
// ❌ BLOQUEA TODO
for (let i = 0; i < 1000000000; i++) {
// Cálculo pesado
}✗ Operaciones síncronas de I/O:
javascript
// ❌ BLOQUEA
const data = fs.readFileSync('large-file.txt');
// ✅ NO BLOQUEA
fs.readFile('large-file.txt', (err, data) => {});✗ Cálculos CPU-intensivos:
javascript
// ❌ BLOQUEA
function fibonacci(n) {
if (n < 2) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
fibonacci(40); // Bloquea todoConsecuencias:
•No puede procesar nuevas requests
•Timers no se ejecutan
•Event loop se congela
•Usuarios experimentan timeout
•Aplicación parece "muerta"
Soluciones:
✓Dividir trabajo:
javascript
// Dividir en chunks
function processChunk(chunk, callback) {
setImmediate(() => {
// Procesar chunk
callback();
});
}✓Worker Threads:
javascript
const { Worker } = require('worker_threads');
const worker = new Worker('./cpu-intensive-task.js');
worker.postMessage(data);✓Child Processes:
javascript
const { fork } = require('child_process');
const child = fork('./heavy-computation.js');
child.send(data);✓Usar async siempre que sea posible:
javascript
// ✅ NO BLOQUEA
await fs.promises.readFile('file.txt');Regla: Mantén el event loop libre, delega trabajo pesado a workers o procesos separados
Manejo de Errores y Debugging
2 preguntas
1
¿Cómo manejar errores async correctamente?
Problema común: Errores en código asíncrono pueden no ser capturados si no se manejan correctamente.
Métodos de manejo:
1. Promises con .catch():
javascript
// ✅ CORRECTO
promiseFunction()
.then(result => {
// éxito
})
.catch(error => {
// manejar error
console.error(error);
});
// ❌ INCORRECTO - Error no capturado
promiseFunction()
.then(result => {
throw new Error('Oops');
});
// Error se pierde!2. async/await con try-catch:
javascript
// ✅ CORRECTO
async function handler() {
try {
const result = await asyncFunction();
return result;
} catch (error) {
console.error('Error:', error);
throw error; // Re-throw si es necesario
}
}3. Error handlers en callbacks:
javascript
// ✅ CORRECTO
fs.readFile('file.txt', (err, data) => {
if (err) {
console.error('Error:', err);
return;
}
// procesar data
});4. process.on('unhandledRejection'):
javascript
// Capturar promesas rechazadas no manejadas
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection:', reason);
// Log, notificar, o cerrar proceso
});Mejores prácticas:
✓Siempre manejar errores:
• Cada async operation debe tener error handling
• No dejar promesas sin .catch()
✓Error boundaries:
javascript
// Wrapper para manejar errores
async function safeAsync(fn) {
try {
return await fn();
} catch (error) {
console.error('Error in safeAsync:', error);
return null;
}
}✓Propagar errores correctamente:
javascript
// Re-throw para que el caller maneje
catch (error) {
logger.error(error);
throw error; // Propagar
}✓Error handling en Express:
javascript
app.use(async (req, res, next) => {
try {
await next();
} catch (error) {
res.status(500).json({ error: error.message });
}
});Errores comunes:
✗ Olvidar .catch() en promesas
✗ No usar try-catch con async/await
✗ Swallow errors (capturar y no hacer nada)
✗ No propagar errores cuando es necesario
2
¿Qué es un memory leak en Node y cómo lo detectas?
Definición: Memoria que ya no se usa pero no se libera, causando aumento gradual del uso de memoria.
Causas comunes:
✓Closures:
javascript
// ❌ LEAK: Referencia a objeto grande en closure
function leak() {
const bigData = new Array(1000000).fill('data');
return function() {
console.log('Still holding reference to bigData');
};
}✓Event Listeners no removidos:
javascript
// ❌ LEAK
const emitter = new EventEmitter();
emitter.on('event', handler);
// Si no se remueve, el handler mantiene referencias
// ✅ CORRECTO
emitter.removeListener('event', handler);
// O usar once() para auto-remover✓Timers no limpiados:
javascript
// ❌ LEAK
const interval = setInterval(() => {}, 1000);
// Si no se limpia, sigue ejecutándose
// ✅ CORRECTO
clearInterval(interval);✓Referencias globales:
javascript
// ❌ LEAK
global.data = largeObject;
// ✅ CORRECTO
// Evitar variables globales o limpiarlasCómo detectar:
✓Node.js Inspector:
bash
node --inspect app.js
# Abrir chrome://inspect✓heapdump:
javascript
const heapdump = require('heapdump');
heapdump.writeSnapshot('/tmp/heap-' + Date.now() + '.heapsnapshot');✓clinic.js:
bash
npm install -g clinic
clinic doctor -- node app.js✓Monitoreo:
javascript
// Ver uso de memoria
console.log(process.memoryUsage());
// { rss, heapTotal, heapUsed, external }Señales de memory leak:
•Uso de memoria aumenta constantemente
•Performance degrada con el tiempo
•Crashes por "out of memory"
•GC frecuente pero memoria no baja
Streams y I/O
1 preguntas
1
¿Cuándo usar streams?
Definición: Streams permiten procesar datos en chunks en lugar de cargar todo en memoria.
Cuándo usar streams:
✓Archivos grandes:
javascript
// ❌ Carga todo en memoria
const data = fs.readFileSync('large-file.txt');
// ✅ Procesa en chunks
fs.createReadStream('large-file.txt')
.pipe(transformStream)
.pipe(fs.createWriteStream('output.txt'));✓Datos de red:
javascript
// ✅ Stream HTTP response
app.get('/download', (req, res) => {
const fileStream = fs.createReadStream('large-file.zip');
fileStream.pipe(res);
});✓Transformación de datos:
javascript
// ✅ Transformar mientras lees
const { Transform } = require('stream');
const upperCase = new Transform({
transform(chunk, encoding, callback) {
callback(null, chunk.toString().toUpperCase());
}
});
readStream.pipe(upperCase).pipe(writeStream);✓Procesamiento de logs:
javascript
// ✅ Procesar línea por línea
const readline = require('readline');
const rl = readline.createInterface({
input: fs.createReadStream('access.log')
});
rl.on('line', (line) => {
// Procesar cada línea
});Tipos de streams:
| Tipo | Descripción | Ejemplo |
|---|---|---|
Readable | Lee datos | fs.createReadStream |
Writable | Escribe datos | fs.createWriteStream |
Duplex | Lee y escribe | net.Socket |
Transform | Transforma datos | crypto.createCipher |
Ventajas:
✓Menor uso de memoria
✓Procesamiento más rápido
✓Puede empezar antes de tener todos los datos
✓Escalable para datos grandes
Cuándo NO usar:
✗ Datos pequeños (< 1MB)
✗ Necesitas acceso aleatorio
✗ Procesamiento simple y rápido
Ejemplo práctico:
javascript
// Procesar CSV grande
const csv = require('csv-parser');
fs.createReadStream('data.csv')
.pipe(csv())
.on('data', (row) => {
// Procesar cada fila
})
.on('end', () => {
console.log('Procesamiento completo');
});REST APIs
5 preguntas
1
¿Cómo versionas las REST APIs?
4 estrategias principales:
| Estrategia | Ejemplo | ✅ Ventajas | ❌ Desventajas |
|---|---|---|---|
URL Path | /api/v1/users | Explícito, fácil de entender | URLs cambian con cada versión |
Query Param | /api/users?version=1 | URLs base no cambian | Menos RESTful, puede ignorarse |
Header | Accept: application/vnd.api.v1+json | URLs limpias | Menos visible, requiere conocimiento |
Media Type | Content-Type: ...;version=1 | Separación de contenido | Similar a header |
✅ Recomendación: URL Path Versioning (
/api/v1/...)Mejores prácticas:
•Mantener versiones anteriores durante transición
•Documentar cambios entre versiones
•Usar versionado semántico (v1, v2) no fechas
2
Explica las APIs idempotentes
Definición: Operación que produce el mismo resultado ejecutándola 1 vez o múltiples veces con los mismos parámetros.
Métodos HTTP:
| Método | ¿Idempotente? | Razón |
|---|---|---|
GET | ✅ Sí | Solo lee, no modifica estado |
PUT | ✅ Sí | Reemplaza completamente el recurso |
DELETE | ✅ Sí | Eliminar algo que no existe = mismo efecto |
PATCH | ❌ No | Cambios parciales pueden acumularse |
POST | ❌ No | Cada llamada puede crear nuevo recurso |
Ejemplo:
PUT /api/users/123
{ "name": "Juan", "email": "juan@example.com" }Ejecutar 1x o 10x → mismo resultado
¿Por qué importa?
✓Reintentos seguros sin efectos secundarios
✓Confiabilidad en redes inestables
✓Facilita retry logic
✓Evita duplicación
Mejores prácticas:
•PUT para actualizaciones completas
•PATCH solo para parciales
•Tokens de idempotencia para POST cuando sea necesario
3
¿Cómo diseñarías una API escalable?
Principios clave para escalabilidad:
1. Arquitectura:
✓Stateless: Sin estado en servidor, usar tokens (JWT)
✓Microservicios: Dividir en servicios independientes
✓Load Balancing: Distribuir carga entre múltiples servidores
✓Caching: Redis para datos frecuentes
✓CDN: Para assets estáticos
2. Base de datos:
✓Índices: Optimizar consultas frecuentes
✓Paginación: No retornar todos los datos
✓Read Replicas: Separar lecturas de escrituras
✓Sharding: Dividir datos por particiones
✓Connection Pooling: Reutilizar conexiones
3. Performance:
✓Rate Limiting: Prevenir abuso
✓Compresión: Gzip/Brotli para respuestas
✓Lazy Loading: Cargar datos bajo demanda
✓Batch Operations: Agrupar operaciones
✓Async Processing: Colas para tareas pesadas
4. Monitoreo y observabilidad:
✓Logging: Centralizado (ELK, Loki)
✓Metrics: Prometheus, Datadog
✓Tracing: Distributed tracing (Jaeger)
✓Health Checks: Endpoints de salud
✓Alerting: Notificaciones automáticas
5. Diseño de endpoints:
javascript
// ✅ Buen diseño
GET /api/v1/users?page=1&limit=20&sort=created_at
POST /api/v1/users
PUT /api/v1/users/:id
DELETE /api/v1/users/:id
// ❌ Mal diseño
GET /api/getAllUsers
POST /api/createUser6. Versionado:
✓URL path versioning (
/api/v1/...)✓Mantener versiones anteriores
✓Documentar cambios
7. Seguridad:
✓HTTPS siempre
✓Autenticación robusta
✓Validación de inputs
✓Rate limiting
✓CORS configurado correctamente
Ejemplo de arquitectura escalable:
[Load Balancer]
↓
[API Servers] (stateless, múltiples instancias)
↓
[Cache Layer] (Redis)
↓
[Database] (Master + Read Replicas)
↓
[Message Queue] (RabbitMQ/Kafka para async)4
¿Qué es HATEOAS?
Definición: Hypermedia As The Engine Of Application State - Principio REST que incluye links en las respuestas para navegar la API.
Concepto: El cliente descubre acciones disponibles a través de links en las respuestas, sin conocer URLs hardcodeadas.
Ejemplo sin HATEOAS:
json
// ❌ Cliente necesita conocer URLs
GET /api/users/123
{
"id": 123,
"name": "Juan"
}
// Cliente debe saber que existe PUT /api/users/123Ejemplo con HATEOAS:
json
// ✅ Links incluidos en respuesta
GET /api/users/123
{
"id": 123,
"name": "Juan",
"_links": {
"self": { "href": "/api/users/123" },
"update": { "href": "/api/users/123", "method": "PUT" },
"delete": { "href": "/api/users/123", "method": "DELETE" },
"orders": { "href": "/api/users/123/orders" }
}
}Ventajas:
✓Desacoplamiento: Cliente no necesita conocer URLs
✓Evolución: API puede cambiar URLs sin romper clientes
✓Descubrimiento: Cliente descubre capacidades dinámicamente
✓Navegación: Similar a navegar una página web
Desventajas:
✗ Más complejo de implementar
✗ Respuestas más grandes
✗ No siempre necesario para APIs simples
Cuándo usar:
✓APIs públicas complejas
✓Cuando URLs pueden cambiar
✓Cuando quieres que el cliente descubra capacidades
✓APIs que evolucionan frecuentemente
Cuándo NO usar:
✗ APIs internas simples
✗ Cuando el overhead no vale la pena
✗ APIs con contratos estables
Formato común:
json
{
"data": {...},
"_links": {
"self": {"href": "/api/resource"},
"next": {"href": "/api/resource?page=2"},
"prev": {"href": "/api/resource?page=1"}
}
}5
REST vs GraphQL (cuándo usar cada uno)
Comparación rápida:
| Aspecto | REST | GraphQL |
|---|---|---|
Estructura | Múltiples endpoints | Un solo endpoint |
Datos | Formato fijo | Cliente especifica qué necesita |
Over-fetching | ❌ Sí, puede traer datos innecesarios | ✅ No, solo lo solicitado |
Under-fetching | ❌ Sí, puede necesitar múltiples requests | ✅ No, todo en una query |
Caching | ✅ Fácil (HTTP caching) | ⚠️ Más complejo |
Complejidad | ✅ Simple | ❌ Más complejo |
Tipado | ❌ No | ✅ Schema fuerte |
Versionado | ✅ Fácil (URLs) | ⚠️ Schema evolution |
REST - Usa cuando:
✓APIs simples: CRUD básico
✓Caching importante: Necesitas HTTP caching
✓Múltiples clientes: Diferentes necesidades
✓APIs públicas: Más fácil de entender
✓Microservicios: Cada servicio su propio endpoint
Ejemplo REST:
javascript
// Múltiples requests
GET /api/users/123
GET /api/users/123/posts
GET /api/users/123/commentsGraphQL - Usa cuando:
✓Cliente móvil: Necesita datos específicos (ahorro de ancho de banda)
✓Datos complejos: Múltiples relaciones
✓Over-fetching problemático: Datos grandes innecesarios
✓Frontend complejo: Necesita flexibilidad
✓Equipo con experiencia: GraphQL tiene curva de aprendizaje
Ejemplo GraphQL:
graphql
# Una sola query
query {
user(id: 123) {
name
posts {
title
comments {
text
}
}
}
}Híbrido:
✓Usar REST para operaciones simples
✓Usar GraphQL para queries complejas
✓GraphQL puede usar REST como fuente de datos
Mejores prácticas:
•REST: Para la mayoría de casos
•GraphQL: Cuando over-fetching es problema real
•Considerar complejidad del equipo
•Evaluar necesidades específicas del proyecto