Ronda 1 · «ver si tienes experiencia con Node»
La vacante oficial de Smartly en CDMX dice literalmente que su backend es Node.js y TypeScript. Eso confirma lo que te dijo Alex y te dice qué van a preguntar. Aquí están las doce preguntas probables con respuesta, y tu evidencia real.
| Alex dijo | La publicación confirma |
|---|---|
| "Trabajamos con Node" | La vacante de Senior Backend Engineer en CDMX: "Our backend stack is built around Node.js and TypeScript" |
| "Mi equipo es Intelligence" | El puesto habla de "data-intensive backend services", "data pipelines" y "platform integrations" |
| "80 % backend, 20 % frontend" | Piden APIs, servicios distribuidos y operación en producción; el frontend no aparece como requisito |
| "Dos entrevistas más" | El formato coincide con lo que se ve en su proceso: una técnica y una de diseño |
| — | Nuevo: 250+ personas en Producto e Ingeniería, 750+ empleados, 24 oficinas en 13 países, híbrido 1 día/semana en CDMX |
Fuente: vacante pública de Senior Backend Engineer, Smartly, Ciudad de México.
Esto es lo que puedes afirmar sin que se caiga en la segunda pregunta:
| Qué | Números verificados |
|---|---|
| API de HeyDuck | 29.247 líneas · 20 controladores · 65 endpoints |
| Stack de esa API | NestJS · Drizzle · PostgreSQL con RLS · BullMQ · Redis |
| conFirma | 636 archivos · 327 commits · Prisma · LangGraph |
| Proyectos de datos en Python | ≈ 9.700 líneas en cuatro repos públicos |
| Plataforma de su trabajo | 173.445 líneas de NestJS — coordinadas, no escritas (30 de 4.958 commits) |
"Yes — I built and operate a NestJS API: about twenty-nine thousand lines, sixty-five endpoints, PostgreSQL with row-level security, BullMQ queues on Redis, and Drizzle as the data layer. That's my own code, not a team's. At work I coordinate a much larger NestJS backend — a hundred and seventy thousand lines — but my contribution there is architecture and code review, not volume, and I say that plainly."
La última frase es la que te hace creíble. Sin ella, la primera pregunta de seguimiento te tumba.
Node corre JavaScript en un solo hilo, y delega I/O al sistema (libuv). El event loop tiene fases: timers → callbacks pendientes → poll (I/O) → check (setImmediate) → close. Entre cada fase se vacían las microtareas (promesas), y process.nextTick corre antes que cualquier microtarea.
Consecuencia práctica: un Promise.resolve().then() se ejecuta antes que un setTimeout(0). Y setImmediate corre en la fase check, después del poll.
Se bloquea con trabajo de CPU o I/O sincrónico: readFileSync, pbkdf2Sync, un JSON.parse de 200 MB, una expresión regular con retroceso catastrófico, un bucle de 10⁸ iteraciones.
Detección: event loop lag (medir con perf_hooks.monitorEventLoopDelay), latencia p99 que sube para todos los usuarios a la vez, no solo para quien llamó.
Arreglo: versión asíncrona, worker_threads para CPU, o mover el trabajo a otro servicio.
Un stream tiene un búfer con highWaterMark. Si el consumidor es más lento que el productor, write() devuelve false: hay que esperar el evento drain. Si ignoras eso, el búfer crece hasta agotar memoria.
En la práctica: stream.pipeline() conecta y propaga errores y cierre correctamente. Es lo que usaría para leer un CSV de 2 GB o reenviar una respuesta sin cargarla en memoria.
cluster: varios procesos que comparten el mismo puerto, para aprovechar núcleos en un servicio web. worker_threads: hilos dentro del mismo proceso con memoria compartida opcional, para CPU. child_process: programa externo, aislamiento total, más coste.
Hoy en la práctica: el escalado horizontal lo hace el orquestador (contenedores); worker_threads solo cuando hay CPU que no se puede sacar del proceso.
Un throw dentro de un callback no lo atrapa el try/catch de fuera. Con promesas hay que await o encadenar .catch(). unhandledRejection en producción es un fallo, no un aviso: se registra y se cierra el proceso con reinicio limpio, mejor que un estado indefinido.
Y para clientes HTTP: errores tipados, no strings. Un error de validación responde 422, uno de negocio 409, uno inesperado 500 sin filtrar el mensaje interno.
Causas típicas: listeners que nunca se quitan (MaxListenersExceededWarning), cachés sin tope, closures que retienen objetos grandes, timers que nunca se limpian, acumular en un array global.
Detección: heap snapshot con dos tomas y comparar; process.memoryUsage().heapUsed creciendo en el tiempo; el RSS subiendo en el panel de métricas.
Al recibir SIGTERM: dejar de aceptar conexiones (server.close()), esperar las peticiones en vuelo, terminar los jobs en curso, cerrar el pool de base de datos y las colas, y salir. Con un timeout máximo para no quedarse colgado.
Y antes: un readiness que diga "no estoy listo" para que el balanceador deje de mandar tráfico.
Semántica al menos una vez: un job se puede ejecutar dos veces, así que el consumidor tiene que ser idempotente. Concurrencia por worker, reintentos con backoff exponencial, y una cola de fallidos (dead letter) que alguien mira.
En HeyDuck usé seis tipos de job: correos, premios, agregados, webhooks. La métrica que importa no es el throughput: es la profundidad de la cola.
Validación en la frontera con esquema (Zod) y tipos derivados, no duplicados. Claves de idempotencia en POST que crean dinero o recursos. Paginación por cursor, no offset. Un formato único de error. Límite de tasa por identidad. Y versionado o compatibilidad hacia atrás explícita.
Un pool, no una conexión por petición; y un tope de conexiones que case con el de la base (el clásico: 20 réplicas × 20 conexiones = 400 conexiones y Postgres ahogado). Transacciones para lo que toca varias tablas. Índices por lo que filtra de verdad. Y cuidado con N+1 al recorrer relaciones.
En HeyDuck añadí RLS: cada fila lleva organization_id y la política de Postgres filtra por el contexto de la sesión. El aislamiento no depende de que el código se acuerde.
Unitarias del dominio puro (sin base, sin red), de integración contra Postgres real (no mocks: los mocks mienten en SQL), y de contrato para la API. Reloj y azar inyectados para que las pruebas sean deterministas.
Y una regla: si una prueba tarda más de un segundo, la gente deja de correrla.
Logs estructurados con un request_id que atraviesa servicios; métricas de latencia p50 y p99 (no media), tasa de error, profundidad de cola y lag del event loop; trazas para seguir una petición entre servicios.
Y la pregunta que hay que poder contestar: "¿cómo sé que está roto antes que el usuario?"
Con un puesto backend en Node, lo probable no es LeetCode difícil, sino lógica con JavaScript moderno y asincronía:
allSettled, un pool con concurrencia limitada, un retry con backoff.AbortController.Practica en pruebas.ened.online: ya tiene promisePool, retry, debounce, once, curry, groupBy y memoize, con pruebas que se ejecutan de verdad.