Inicio / Artículos

Preparación para entrevistas de Node.js y Express: 70 preguntas con respuestas precisas.

Una revisión estructurada de 70 preguntas de entrevista sobre Node.js y Express, que abarcan desde el bucle de eventos y los flujos hasta el middleware, la seguridad y la escalabilidad, con correcciones a respuestas comunes.

Editor: REACTAPP.TOP21 min de lectura4861 palabras

Fuentes: Node.js & Express.js Interview Guide

Las entrevistas de backend para puestos en Node.js suelen girar en torno a los mismos temas: cómo un entorno de ejecución single-threaded maneja miles de conexiones, qué hace realmente el bucle de eventos y cómo Express convierte a un servidor HTTP básico en una aplicación. Las respuestas memorizadas fallan ante preguntas adicionales, por lo que esta guía agrupa 70 preguntas comunes por tema y proporciona para cada una una explicación suficiente que permita defenderla. Cuando la respuesta popular está desactualizada o es imprecisa, se indica la corrección, ya que identificar esos detalles suele ser lo que diferencia a un candidato sólido de uno solo preparado.

Conceptos básicos de Node.js

P1. ¿Qué es Node.js?

Es un entorno de ejecución que procesa JavaScript fuera del navegador. Incluye el motor V8 de Google y añade APIs para archivos, redes y procesos. Su modelo está basado en eventos con entrada/salida no bloqueante, lo que lo hace ligero y adecuado para servicios con grandes volúmenes de datos en tiempo real.

P2. ¿Es Node.js de un solo hilo?

Tu JavaScript se ejecuta en un único hilo principal, el que impulsa el bucle de eventos. Debajo de él, libuv mantiene un pool de hilos para tareas que no cuentan con una buena interfaz asíncrona del sistema operativo, como las llamadas al sistema de archivos, dns.lookup, algunas funciones crypto y zlib. Por lo tanto, la respuesta precisa es: de un solo hilo para tu código, pero multi-hilo a nivel del sistema.

P3. ¿Por qué es rápido Node.js?

  • V8 compila JavaScript a código máquina en lugar de interpretarlo.
  • La entrada y la salida son asíncronas y no bloqueantes.
  • El bucle de eventos proporciona concurrencia sin necesidad de un hilo por solicitud.
  • El modelo destaca en tareas limitadas por la red o el disco; las tareas limitadas por la CPU son su punto débil.
  • P4. Código síncrono versus asíncrono

    Una operación síncrona finaliza antes de que se ejecute la siguiente línea, bloqueando todo lo que sigue. Una operación asíncrona, como la lectura de un archivo o una consulta a una base de datos, se inicia y se pasa a otro proceso; la ejecución continúa y un callback o promise entrega el resultado más tarde. En la versión síncrona que se muestra a continuación, el registro espera a que termine toda la lectura (y imprime un Buffer, ya que no se proporciona codificación):

    // Synchronous
    const data = fs.readFileSync('file.txt');
    console.log(data);
    

    En la versión asíncrona, la última línea de registro se imprime antes del contenido del archivo, porque la lectura finaliza en una iteración posterior del bucle de eventos:

    // Asynchronous
    fs.readFile('file.txt', (err, data) => {
      console.log(data);
    });
    console.log("This runs first!");
    

    P5. ¿Qué es npm?

    npm es el gestor de paquetes que viene incluido con Node.js. Se utiliza para instalar, publicar y gestionar dependencias, y las registra junto con los scripts en package.json.

    P6. package.json versus package-lock.json

    package.json describe el proyecto: su nombre, versión, punto de entrada, scripts y los rangos de dependencias que acepta. package-lock.json fija la versión exacta de cada paquete instalado, incluidos los transitivos, para que todas las máquinas y ejecuciones de CI instalen un árbol idéntico. Guarda ambos archivos.

    P7. ¿Qué módulos principales deberías conocer?

    Los componentes integrados más mencionados incluyen fs, path, http y https, os, events, stream, buffer, crypto, util, url, querystring (que ahora se considera obsoleto en favor de URLSearchParams), child_process, cluster y zlib.

    P8. require() versus import

    Las diferencias entre CommonJS y los módulos ES surgen constantemente:

    • require se carga de forma síncrona; los módulos ES se cargan de forma asíncrona y también ofrecen import() dinámico que devuelve una promesa.
    • CommonJS es el formato predeterminado para los archivos .js; los módulos ES requieren la opción "type": "module" en package.json o la extensión .mjs.
  • CommonJS exporta mediante module.exports; los módulos ES utilizan export y export default.
  • require puede llamarse en cualquier lugar, incluso dentro de condiciones; las declaraciones estáticas import se elevan al nivel superior y deben estar en la parte superior del código.
  • P9. module.exports versus exports

    exports comienza como una referencia al mismo objeto que module.exports. Añadir propiedades a él funciona, pero asignar un nuevo valor (exports = {...}) solo vuelve a vincular la variable local, y el módulo sigue exportando el objeto original. Para reemplazar todo el contenido de exportación, hay que asignar a module.exports.

    P10. ¿Qué es el REPL?

    REPL es la abreviatura de Read, Eval, Print, Loop. Al ejecutar node sin argumentos se abre una shell interactiva que evalúa cada expresión que escribes y muestra el resultado, lo cual resulta útil para experimentos rápidos.

    El bucle de eventos y los patrones asíncronos

    P11. ¿Qué es el bucle de eventos?

    Es el mecanismo que permite a un único hilo de JavaScript realizar operaciones de entrada/salida sin bloqueos. Cuando la pila de llamadas está vacía, el bucle toma el siguiente callback listo de sus colas y lo ejecuta. Un análisis más detallado sobre cómo interactúa con libuv se encuentra en nuestro artículo sobre libuv, el bucle de eventos y el pool de hilos.

    P12. ¿Cuáles son las fases del bucle de eventos?

    1. Timers: callbacks de setTimeout y setInterval cuyo tiempo ha llegado.
  • Callback pendientes: ciertos callbacks de operaciones del sistema pospuestos desde la iteración anterior.
  • Inactivo y preparación: tareas internas de mantenimiento.
  • Recolección: recopila los eventos de entrada/salida recién completados y ejecuta sus callbacks.
  • Verificación: callbacks de setImmediate().
  • Callbacks de cierre: manejadores como socket.on('close').
  • Las colas de microtareas (process.nextTick primero, luego las reacciones de promesas) se vacían después de cada callback individual, no solo entre fases. Ese ha sido el comportamiento desde Node 11, y es una pregunta frecuente al respecto.

    P13. nextTick, promesas, setTimeout y setImmediate

    • Los callbacks de process.nextTick() se ejecutan tan pronto como finaliza la operación actual, antes que las microtareas de promesas.
  • Las reacciones prometidas como .then se ejecutan una vez que la cola de nextTick está vacía, pero aún antes de que el bucle continúe.
  • setTimeout(fn, 0) se dispara durante la fase de temporizadores una vez que ha transcurrido su retraso mínimo.
  • setImmediate() se dispara durante la fase de verificación, justo después de la fase de consulta de la misma iteración.
  • Este script utiliza los cuatro métodos:

    console.log('start');
    setTimeout(() => console.log('setTimeout'), 0);
    setImmediate(() => console.log('setImmediate'));
    process.nextTick(() => console.log('nextTick'));
    Promise.resolve().then(() => console.log('promise'));
    console.log('end');
    

    La salida esperada muestra primero los registros síncronos y luego las microtareas. Las dos macrotareas pueden aparecer en cualquier orden cuando se programan desde el módulo principal, ya que depende de si ha transcurrido el umbral del temporizador al inicio del bucle; dentro de una función de callback de archivo o socket, setImmediate siempre tiene prioridad:

    // Output:
    // start
    // end
    // nextTick
    // promise
    // setTimeout (or setImmediate — order can vary if not inside I/O)
    // setImmediate (or setTimeout)
    

    Q14. Callbacks y el infierno de callbacks

    Una función de callback es una función que se pasa a una operación para que pueda ser llamada cuando dicha operación finalice. Cuando cada paso depende del anterior, las funciones de callback se anidan una tras otra, generando la estructura en forma de pirámide conocida como “infierno de callbacks”, lo cual dificulta la lectura y el manejo de errores en:

    getUser(id, (user) => {
      getPosts(user, (posts) => {
        getComments(posts, (comments) => {
          // deeply nested - hard to manage
        });
      });
    });
    

    Las Promesas y async/await simplifican esta estructura.

    P15. ¿Qué es una Promesa?

    Una Promesa es un objeto que representa el resultado futuro de una operación asíncrona. Puede estar en estado pendiente, cumplida con un valor o rechazada por alguna razón, y una vez establecida nunca cambia de estado. Se adjuntan manejadores con .then y .catch:

    const promise = new Promise((resolve, reject) => {
      if (success) resolve(data);
      else reject(error);
    });
    promise.then(data => console.log(data)).catch(err => console.error(err));
    

    P16. ¿Qué es async/await?

    Se trata de una sintaxis basada en promesas que permite al código asíncrono leerse de arriba hacia abajo. Una función async siempre devuelve una promesa, y await pausa dicha función hasta que la promesa esperada se resuelva, siendo los rechazos detectados como excepciones que se pueden capturar:

    async function getUser() {
      try {
        const res = await fetch('/api/user');
        const data = await res.json();
        return data;
      } catch (err) {
        console.error(err);
      }
    }
    

    Tenga en cuenta que este ejemplo registra el error e implica devolver undefined, por lo que quienes llaman a la función no pueden distinguir entre un fallo y un resultado vacío. Re lanzar la excepción suele ser la mejor opción.

    Q17. Combinadores de promesas

    • Promise.all() se resuelve cuando todas las entradas se resuelven y rechaza tan pronto como alguna entrada se rechaza.
    • Promise.race() se resuelve con la primera entrada que se resuelva, ya sea de forma satisfactoria o rechazada.
    • Promise.allSettled() espera a que todas las entradas se resuelvan e informa sobre cada resultado.
  • Promise.any() se cumple con el primer valor que se cumpla y rechaza (con un AggregateError) solo si todos se rechazan.
  • P18. Bloqueante versus no bloqueante

    El código bloqueante detiene todo lo demás hasta que finaliza, como lo hace fs.readFileSync. El código no bloqueante inicia el trabajo y permite que el programa continúe, como lo hace fs.readFile. En un servidor, una llamada bloqueante detiene todas las solicitudes en curso.

    Flujos, búferes y eventos

    P19. ¿Qué son los flujos?

    Los flujos procesan los datos de forma incremental, por trozos, en lugar de almacenarlos todos en memoria. Son la herramienta adecuada para archivos grandes, medios y tráfico de red. Existen cuatro tipos:

    • Lectibles, como fs.createReadStream
    • Escribibles, como fs.createWriteStream
    • Dúplex, legible y escribible al mismo tiempo, como un socket TCP
    • Transform, un flujo dúplex que modifica los datos a medida que pasan, como zlib.createGzip

    P20. ¿Qué es un Buffer?

    Un Buffer almacena datos binarios en bruto en memoria asignada fuera del heap de V8. Lo encuentras dondequiera que los datos estén en formato de bytes y no como cadenas o objetos: flujos, archivos, sockets. Al crear uno a partir de una cadena, se muestran los valores de byte subyacentes en hexadecimal:

    const buf = Buffer.from('Hello');
    console.log(buf); // <Buffer 48 65 6c 6c 6f>
    

    P21. ¿Qué es EventEmitter?

    EventEmitter del módulo events implementa el patrón de observador (pub/sub) en el que se basa gran parte de la API de Node. Se crea una instancia:

    const EventEmitter = require('events');
    const emitter = new EventEmitter();
    

    luego se registran los oyentes con on y se activan con emit, pasando cualquier argumento junto con ellos:

    emitter.on('greet', (name) => console.log(`Hello, ${name}`));
    emitter.emit('greet', 'John');
    

    Los listeners se ejecutan de forma síncrona, en el orden en que se registraron, cuando se llama a emit.

    P22. ¿Cómo se procesan grandes cantidades de datos sin agotar la memoria?

    Utilice flujos: lea trozos pequeños y conduzca un flujo legible a uno escribible, de modo que el uso de memoria se mantenga más o menos constante independientemente del tamaño del archivo:

    fs.createReadStream('input.txt').pipe(fs.createWriteStream('output.txt'));
    

    Para código de producción, stream.pipeline es preferible a .pipe() porque propaga los errores y limpia cada flujo de la cadena.

    Conceptos avanzados de tiempo de ejecución

    P23. process.nextTick() versus setImmediate(), en profundidad

    nextTick ejecuta sus callbacks antes de que se permita continuar el bucle, incluso antes que los microtareas de las promesas. Los callbacks de setImmediate esperan hasta la fase de verificación, después de la fase de monitoreo. Dado que cada nextTick puede programar otro, una cadena recursiva puede agotar el bucle de tal manera que las lecturas y escrituras pendientes nunca se procesen. Ese modo de fallo se analiza en cómo process.nextTick agota silenciosamente el bucle de eventos de Node.js.

    P24. ¿Qué es el clustering y por qué usarlo?

    Un único proceso de Node utiliza un núcleo para JavaScript. El módulo cluster crea varios procesos trabajadores, cada uno con su propio bucle de eventos y memoria, y les permite compartir un puerto del servidor para que todos los núcleos de la máquina atiendan el tráfico. Se importan los módulos de la siguiente manera:

    const cluster = require('cluster');
    const os = require('os');
    

    Luego se crea un trabajador por CPU a partir del proceso principal, mientras los trabajadores cargan el servidor:

    if (cluster.isMaster) {
      os.cpus().forEach(() => cluster.fork());
    } else {
      require('./server');
    }
    

    cluster.isMaster sigue funcionando pero ha sido obsoleto en favor de cluster.isPrimary; utilice este último en el código nuevo.

    P25. cluster versus worker_threads

    • cluster crea procesos separados con memoria aislada, lo cual es adecuado para escalar servidores HTTP.
    • worker_threads crea hilos dentro de un mismo proceso. Pueden compartir memoria mediante SharedArrayBuffer o transferir buffers, lo que es adecuado para tareas intensivas en CPU como el procesamiento de imágenes.

    P26. ¿Cómo se manejan las tareas intensivas en CPU?

    Los bloques de código que dependen excesivamente de la CPU bloquean el bucle de eventos, por lo que se deben trasladar a otro lugar: a worker_threads, a un proceso hijo, o fuera del servicio por completo mediante una cola y trabajadores en segundo plano. Consulte cómo transferir tareas intensivas de CPU con worker threads y pools para conocer patrones al respecto.

    P27. ¿Qué es el middleware en general?

    El middleware es código que se encuentra entre la recepción de una solicitud y el envío de la respuesta, donde puede inspeccionar, transformar, validar o registrar información. Express construye todo su diseño en torno a esta idea, como se muestra en las secciones siguientes de Express.

    P28. ¿Qué son los procesos hijos?

    El módulo child_process ejecuta programas externos u otros scripts de Node en procesos separados. Cuatro funciones son importantes:

    • spawn() transmite los resultados en flujo, por lo que maneja bien salidas de gran tamaño.
  • exec() ejecuta a través de un shell y almacena temporalmente la salida, lo cual es adecuado para resultados pequeños.
  • execFile() ejecuta un archivo directamente sin usar shell, lo que resulta más seguro con argumentos no confiables.
  • fork() es una versión especializada de spawn para módulos Node que establece un canal IPC para el intercambio de mensajes.
  • P29. ¿Qué es libuv?

    libuv es la biblioteca en C que proporciona el bucle de eventos de Node, la entrada/salida asíncrona y el pool de hilos, ocultando las diferencias entre los mecanismos de notificación de eventos de Linux, macOS y Windows.

    P30. setTimeout versus setInterval

    setTimeout(fn, delay) ejecuta fn una vez después de al menos delay milisegundos. setInterval(fn, delay) la ejecuta repetidamente a ese intervalo hasta que se llama a clearInterval().

    Q31. Manejo de excepciones no capturadas y rechazos sin manejo

    Existen manejadores a nivel de proceso para ambos casos. En el caso de excepciones síncronas que nadie capturó, se debe registrar un mensaje y finalizar el proceso, ya que este podría encontrarse en un estado inconsistente:

    process.on('uncaughtException', (err) => {
      console.error('Uncaught Exception:', err);
      process.exit(1); // recommended: exit after cleanup
    });
    

    En el caso de promesas rechazadas sin un manejador:

    process.on('unhandledRejection', (reason, promise) => {
      console.error('Unhandled Rejection:', reason);
    });
    

    Desde Node 15, un rechazo sin manejo hace que el proceso se cierre por defecto, por lo que registrar este listener cambia ese comportamiento. Trátelos ambos como registro de último recurso, no como parte del flujo de control; utilice try/catch y .catch() directamente en el código.

    Q32. ¿Qué es una fuga de memoria y cómo se detecta?

    Una fuga de memoria ocurre cuando esta sigue siendo accesible después de ya no ser necesaria: cachés globales en aumento, temporizadores o listeners que nunca se eliminan, cierres que almacenan objetos grandes. Diagnostíquela con la bandera --inspect y las capturas de memoria de Chrome DevTools, o con herramientas como heapdump y clinic.js, comparando las capturas a lo largo del tiempo.

    P33. __dirname versus __filename

    __dirname es la ruta absoluta de la carpeta que contiene el módulo actual; __filename es la ruta absoluta del propio archivo. Ambos existen solo en CommonJS. En los módulos ES, obténgalos a partir de import.meta.url, o utilice import.meta.dirname y import.meta.filename en versiones recientes de Node.

    P34. ¿Por qué usar el módulo path?

    path permite construir y analizar rutas de archivos de manera portátil a través de funciones como path.join, path.resolve, path.basename y path.extname; así, nunca es necesario concatenar cadenas manualmente ni lidiar con las barras invertidas de Windows frente a las barras normales de Unix.

    P35. fs.readFile versus fs.createReadStream

    fs.readFile carga todo el archivo en memoria antes de devolver un resultado, lo cual es arriesgado con archivos grandes. fs.createReadStream, por su parte, entrega el archivo en trozos, manteniendo el uso de memoria bajo y permitiendo que el procesamiento comience de inmediato.

    Conceptos esenciales de Express

    P36. ¿Qué es Express?

    Express es un framework web mínimo y sin prejuicios que se basa en el módulo http de Node. Agrega funcionalidades de enrutamiento, un pipeline de middleware e integración con motores de plantillas, eliminando la mayor parte de la estructura genérica necesaria para manejar solicitudes de forma básica.

    P37. Un servidor Express básico

    Cree un objeto de aplicación:

    const express = require('express');
    const app = express();
    

    Luego registre una ruta y comience a escuchar:

    app.get('/', (req, res) => {
      res.send('Hello World');
    });app.listen(3000, () => console.log('Server running on port 3000'));
    

    P38. ¿Qué es el middleware en Express?

    El middleware de Express es una función que recibe req, res y next. Puede ejecutar cualquier código, modificar la solicitud o la respuesta, finalizar el ciclo respondiendo o llamar a next() para pasar el control a la función siguiente. Un registrador de solicitudes es el ejemplo clásico:

    app.use((req, res, next) => {
      console.log(`${req.method} ${req.url}`);
      next();
    });
    

    Si un middleware ni responde ni llama a next(), la solicitud se queda atascada.

    P39. Tipos de middleware en Express

    1. A nivel de aplicación, asociado con app.use() o app.METHOD().
    2. A nivel de enrutador, asociado a una instancia de express.Router().
  • Manejo de errores, reconocido por sus cuatro parámetros (err, req, res, next).
  • Internos, como express.json() y express.static().
  • De terceros, como cors, morgan y helmet.
  • P40. ¿Cómo funciona el enrutamiento?

    Un camino relaciona un método HTTP con una ruta y un manejador. Un recurso CRUD típico se ve así:

    app.get('/users', (req, res) => { /* get all users */ });
    app.post('/users', (req, res) => { /* create user */ });
    app.put('/users/:id', (req, res) => { /* update user */ });
    app.delete('/users/:id', (req, res) => { /* delete user */ });
    

    P41. ¿Qué es express.Router()?

    Un router es una miniaplicación autónoma con sus propios caminos y middleware, que se monta en una ruta específica. Permite organizar aplicaciones grandes por funcionalidad. Defina los caminos en su propio módulo:

    // userRoutes.js
    const router = require('express').Router();
    router.get('/', (req, res) => res.send('All users'));
    module.exports = router;
    

    y úselo en la aplicación principal, donde su / se convierte en /users:

    // app.js
    app.use('/users', require('./userRoutes'));
    

    P42. app.use() frente a app.get() y similares

    app.use() coincide con cada método HTTP y cualquier ruta que comience con el prefijo especificado, y por eso se utilizan para montar middleware y enrutadores. app.get(), app.post() y similares coinciden con un método específico y el patrón de ruta completo.

    P43. req.params, req.query y req.body

    • req.params almacena los segmentos de ruta con nombre: /users/:id proporciona req.params.id.
    • req.query almacena los valores de la cadena de consulta: /users?age=25 proporciona req.query.age, siempre como cadena de texto.
    • req.body almacena el contenido del pedido analizado para métodos como POST y PUT, y permanece indefinido a menos que primero se ejecute middleware de análisis de cuerpo como express.json().

    P44. Análisis de JSON y datos de formulario

    Dos analizadores integrados cubren la mayoría de los casos: uno para cuerpos JSON y otro para formularios HTML codificados con URL:

    app.use(express.json());               // parses application/json
    app.use(express.urlencoded({ extended: true })); // parses form data
    

    Q45. ¿Qué es express.static()?

    Es un middleware integrado que sirve archivos como imágenes, hojas de estilo, scripts y HTML desde un directorio:

    app.use(express.static('public'));
    

    Express intermedio

    Q46. Manejo de errores

    Los errores se manejan mediante un middleware con cuatro parámetros, registrado después de todas las rutas:

    app.use((err, req, res, next) => {
      console.error(err.stack);
      res.status(500).json({ message: 'Something went wrong' });
    });
    

    Dentro de las rutas y el middleware, pasa los fallos a él con next(err):

    app.get('/user/:id', async (req, res, next) => {
      try {
        const user = await getUser(req.params.id);
        res.json(user);
      } catch (err) {
        next(err); // passed to error-handling middleware
      }
    });
    

    Evita enviar err.stack o mensajes internos a los clientes en entornos de producción.

    Q47. Manejo de 404

    Express no tiene un manejo integrado para errores 404 en formato JSON. Agrega un middleware general después de cada ruta; si una solicitud llega hasta él, significa que nada más coincidió:

    app.use((req, res) => {
      res.status(404).json({ message: 'Route not found' });
    });
    

    Q48. CORS y el paquete cors

    Cross-Origin Resource Sharing es el mecanismo del navegador que decide si una página de un origen puede leer respuestas de otro. El navegador lo aplica; el servidor se compromete a ello mediante encabezados de respuesta, los cuales el paquete cors configura por usted:

    const cors = require('cors');
    app.use(cors()); // allow all origins
    app.use(cors({ origin: 'https://example.com' })); // restrict origin
    

    El fragmento muestra dos alternativas: permitirlo para todos los orígenes o solo para uno; utilice una u otra, no ambas, ya que la opción más permisiva se aplicaría primero.

    Q49. Sesiones y cookies

    Cargue los middleware para cookies y sesiones:

    const cookieParser = require('cookie-parser');
    const session = require('express-session');
    

    luego regístrelos con una clave secreta y opciones específicas:

    app.use(cookieParser());
    app.use(session({
      secret: 'mySecretKey',
      resave: false,
      saveUninitialized: true
    }));
    

    Guarde la clave secreta en una variable de entorno en lugar de en el código fuente, y considere usar saveUninitialized: false para evitar crear sesiones para visitantes anónimos. El almacenamiento en memoria por defecto no está diseñado para entornos de producción.

    Q50. Autenticación JWT

    Importar la biblioteca:

    const jwt = require('jsonwebtoken');
    

    El código a continuación firma un token que contiene el ID del usuario al iniciar sesión, protege las rutas con un middleware que extrae el token de autenticación del encabezado Authorization y lo verifica, y adjunta la carga descodificada a req.user:

    // Generate token on login
    const token = jwt.sign({ id: user.id }, 'secretKey', { expiresIn: '1h' });// Middleware to protect routes
    function auth(req, res, next) {
      const token = req.headers.authorization?.split(' ')[1];
      if (!token) return res.status(401).json({ message: 'No token' });
      try {
        req.user = jwt.verify(token, 'secretKey');
        next();
      } catch (err) {
        res.status(403).json({ message: 'Invalid token' });
      }
    }app.get('/protected', auth, (req, res) => res.json({ data: 'secret data' }));
    

    Al igual que con las sesiones, la 'secretKey' codificada de forma fija debería provenir de la configuración. Es una convención común devolver 401 cuando falta el token y 403 cuando es inválido, aunque muchas APIs devuelven 401 en ambos casos.

    Q51. Validación de solicitudes

    Bibliotecas como express-validator o joi declaran reglas y recopilan errores. Con express-validator se importan las herramientas auxiliares:

    const { body, validationResult } = require('express-validator');
    

    luego se encadenan los middleware de reglas antes del manejador y se verifica el resultado:

    app.post('/user',
      body('email').isEmail(),
      body('password').isLength({ min: 6 }),
      (req, res) => {
        const errors = validationResult(req);
        if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
        // proceed
      }
    );
    

    Para una comparación más detallada de los enfoques de validación, consulte Zod versus express-validator.

    P52. Cargas de archivos

    multer analiza los datos en formato multipart/form-data. Configure un destino:

    const multer = require('multer');
    const upload = multer({ dest: 'uploads/' });
    

    y aplíquelo por ruta; upload.single('file') espera un archivo en el campo denominado file y lo expone como req.file:

    app.post('/upload', upload.single('file'), (req, res) => {
      res.json({ file: req.file });
    });
    

    En sistemas reales, también limite el tamaño y tipo de los archivos, y nunca confíe en el nombre del archivo proporcionado por el cliente.

    P53. Conexión a MongoDB con Mongoose

    Abra una conexión:

    const mongoose = require('mongoose');
    mongoose.connect('mongodb://localhost:27017/mydb');
    

    luego defina un modelo y úselo desde una ruta:

    const User = mongoose.model('User', new mongoose.Schema({
      name: String,
      email: String
    }));app.post('/users', async (req, res) => {
      const user = await User.create(req.body);
      res.status(201).json(user);
    });
    

    Al pasar directamente req.body a create, los clientes pueden establecer cualquier campo del esquema, por lo que es necesario validar o seleccionar primero los campos permitidos.

    Q54. Orden de ejecución de los middleware

    Express ejecuta los middleware en el orden en que fueron registrados, de arriba hacia abajo, y la cadena se detiene en cualquier función que no llame a next(). Ese orden es la razón por la cual los manejadores de 404 y los manejadores de errores se colocan al final.

    Q55. Motores de plantillas

    Los motores de plantillas generan HTML a partir de datos del servidor. Las opciones más populares son EJS, Pug (anteriormente Jade) y Handlebars. Para configurar uno se necesita un parámetro de configuración del motor de vistas y una llamada a res.render:

    app.set('view engine', 'ejs');
    app.get('/', (req, res) => res.render('index', { name: 'John' }));
    

    Express y Node avanzados

    Q56. Errores asíncronos en Express 4 frente a Express 5

    En Express 4, una promesa rechazada por un manejador async no es capturada por el framework; sin try/catch junto con next(err) o un contenedor, se convierte en un rechazo no manejado. Express 5 redirige automáticamente las promesas rechazadas por los manejadores y el middleware al manejador de errores. La solución habitual en Express 4 es utilizar una pequeña función de orden superior:

    // Express 4 workaround
    const asyncHandler = fn => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
    

    Al envolver un manejador con esta función, cualquier rechazo se dirige a next:

    app.get('/data', asyncHandler(async (req, res) => {
      const data = await fetchData(); // if this throws, asyncHandler catches it
      res.json(data);
    }));
    

    Este patrón se explica en detalle en eliminar el código genérico de try/catch con asyncHandler.

    Q57. Estructuración de una aplicación Express grande

    Un diseño típico al estilo MVC separa las funciones en carpetas:

    project/
    ├── controllers/     # business logic
    ├── routes/          # route definitions
    ├── models/          # DB schemas
    ├── middlewares/      # custom middleware
    ├── config/          # DB, env configs
    ├── utils/           # helper functions
    ├── app.js
    └── server.js
    

    Las rutas declaran los puntos finales y delegan la gestión a los controladores, estos últimos albergan la lógica de las solicitudes, mientras que los modelos encapsulan el acceso a los datos. Separar app.js (que configura la aplicación) de server.js (que la inicia) también facilita su importación en pruebas.

    Q58. Limitación de frecuencia

    express-rate-limit cuenta las solicitudes por cliente dentro de un período determinado. Ímplotalo:

    const rateLimit = require('express-rate-limit');
    

    y aplique un limitador; en este caso, 100 solicitudes por IP cada 15 minutos:

    const limiter = rateLimit({
      windowMs: 15 * 60 * 1000, // 15 minutes
      max: 100 // limit each IP to 100 requests per window
    });app.use(limiter);
    

    El almacén por defecto está en memoria, por lo que cada proceso agrupado cuenta de forma independiente; utilice un almacén compartido como Redis cuando escale la aplicación. Detrás de un proxy, configure trust proxy para que se utilice la IP real del cliente.

    Q59. Seguridad de una aplicación Express

    • Establezca encabezados HTTP de protección con helmet.
  • Valide y sane todos los datos de entrada para bloquear inyecciones SQL o NoSQL así como XSS.
  • Servir mediante HTTPS.
  • Aplicar limitación de frecuencia de solicitudes.
  • Cargar los datos confidenciales desde el entorno, por ejemplo con dotenv, en lugar de guardarlos en el código.
  • Mantener las dependencias actualizadas y ejecutar npm audit.
  • Agregar protección CSRF para aplicaciones basadas en cookies y formularios.
  • P60. res.send() versus res.json() versus res.end()

    • res.send() acepta cadenas de texto, objetos o buffers y elige un Content-Type adecuado.
    • res.json() serializa su argumento como JSON y establece Content-Type: application/json.
    • res.end() finaliza la respuesta sin más datos; es una capa sencilla sobre el método propio de Node.

    P61. ¿Qué hace next('route')?

    Sale por alto las llamadas de retorno restantes de la ruta actual y pasa directamente a la siguiente ruta que coincida. Solo se aplica a los controladores registrados mediante app.METHOD() o router.METHOD() que tengan varias llamadas de retorno.

    P62. Mejora del rendimiento

    • Ejecuta un proceso por núcleo usando clustering o PM2.
    • Almacena en caché las respuestas en Redis o en memoria.
    • Compresiona las respuestas con el middleware compression, o deja que lo haga un proxy.
    • Coloca Nginx u otro proxy inverso o balanceador de carga delante.
    • Afasta las tareas que consumen mucho CPU del bucle de eventos.
    • Reutiliza las conexiones a la base de datos.
    • Considera HTTP/2.
    • Utiliza entrada/salida asíncrona en todo momento y evita las variantes Sync en los caminos de solicitud.

    P63. REST versus GraphQL con Express

    REST expone muchos puntos de extremo con estructuras de respuesta fijas, por lo que los clientes a menudo obtienen más datos de los necesarios o menos. GraphQL expone un único punto de extremo donde el cliente describe con exactitud los datos que desea. Por lo general, se integra en Express mediante express-graphql o apollo-server-express; sin embargo, ambos paquetes han sido obsoletos en favor de integraciones más recientes, por lo que es necesario consultar la documentación actual de Apollo o GraphQL Yoga.

    Q64. ¿Cómo maneja Express las solicitudes concurrentes?

    Cada solicitud es simplemente una serie de devoluciones en el bucle de eventos. Dado que las llamadas a la base de datos y a la red no son bloqueantes, un hilo puede intercalar muchas solicitudes: el tráfico de red se delega a las capacidades asíncronas del sistema operativo, el trabajo con archivos al pool de hilos libuv, y las devoluciones se ejecutan a medida que llegan los resultados. No existe un hilo por solicitud como en los servidores multihilo clásicos, y esa es también la razón por la cual un manejador síncrono lento afecta a todos.

    P65. PM2 frente al módulo cluster

    cluster es la primitiva integrada para el escalado multiproceso. PM2 es un gestor de procesos basado en la misma idea que añade reinicios automáticos tras caídas, equilibrio de carga, manejo de registros, recargas sin interrupciones y monitoreo, razón por la cual se utiliza en tantas implementaciones en producción.

    P66. ¿Por qué colocar un proxy inverso delante de Node?

    Un proxy inverso como Nginx finaliza el protocolo TLS, distribuye la carga entre las instancias, almacena en caché, comprime y sirve archivos estáticos, liberando así esa tarea de Node para que el proceso de la aplicación pueda centrarse en la lógica empresarial.

    P67. Configuración específica del entorno

    El paquete dotenv carga las variables de un archivo .env en process.env, y la aplicación recurre a los valores predeterminados cuando falta un valor:

    require('dotenv').config();
    const PORT = process.env.PORT || 3000;
    

    Las versiones recientes de Node también pueden cargar archivos .env de forma nativa con la opción --env-file. Nunca suba a control de versiones archivos .env que contengan información confidencial.

    P68. app.all() versus app.use()

    app.all() se aplica a cada método HTTP en una ruta específica. app.use() también se aplica a cada método, pero a la ruta y todo lo que está debajo de ella, por lo que es la herramienta adecuada para instalar middleware.

    Q69. Paginación con Express y MongoDB

    Lea los valores de page y limit de la cadena de consulta con valores por defecto razonables, y luego combine skip y limit:

    app.get('/posts', async (req, res) => {
      const page = parseInt(req.query.page) || 1;
      const limit = parseInt(req.query.limit) || 10;
      const posts = await Post.find()
        .skip((page - 1) * limit)
        .limit(limit);
      res.json(posts);
    });
    

    Refuerce la implementación antes de su uso en producción: pase un radicando a parseInt, limite page al menos en 1 y limit a un valor máximo, y agregue un ordenamiento estable (sort). En colecciones muy grandes, skip se vuelve más lento a medida que aumenta el desplazamiento, mientras que la paginación basada en cursor en un campo indexado escala mejor.

    Q70. ¿Qué es la idempotencia?

    Una operación es idempotente cuando repetirla deja al servidor en el mismo estado que realizarla una sola vez. GET, PUT y DELETE están definidos como idempotentes; POST generalmente no lo es, ya que cada llamada puede crear otro recurso. Las reintentos después de los tiempos de espera solo son seguros para las solicitudes idempotentes, por lo que las APIs de pago suelen agregar claves de idempotencia a las solicitudes POST.

    Puntos clave

    • La mayoría de las preguntas sobre Node se reducen a un modelo: un único hilo de JavaScript, un bucle de eventos con fases definidas, microtareas eliminadas después de cada callback, y libuv que se encarga del resto.
    • La mayoría de las preguntas sobre Express se reducen a otro modelo: una cadena ordenada de middleware donde cada función o bien responde o llama a next(), y los errores se dirigen a manejadores de cuatro argumentos.
  • Los entrevistadores valoran la precisión en los detalles que han cambiado con el tiempo, como cluster.isPrimary, las caídas por rechazos no manejados, los errores asíncronos de Express 5 y las funciones auxiliares de rutas ESM.
  • Asocie cada respuesta con una advertencia relacionada con el entorno de producción (secretos en la configuración, propagación de errores, almacenes compartidos de límites de tasa), ya que esa suele ser la siguiente pregunta.