
Conexión a base de datos MySQL
La conexión a bases de datos es una funcionalidad esencial en aplicaciones Node.js que requieren persistencia de datos. MySQL, uno de los sistemas de gestión de bases de datos más utilizados en el entorno empresarial, cuenta con excelente soporte en el ecosistema de Node.js a través del módulo mysql2.
El módulo mysql2 es una biblioteca nativa que proporciona una interfaz completa para interactuar con bases de datos MySQL desde Node.js. A diferencia de su predecesor mysql, mysql2 ofrece mejor rendimiento, soporte para promesas nativas y compatibilidad con las últimas características de MySQL.
En este tutorial utilizamos un caso de uso empresarial real: una aplicación interna que gestiona empleados y facturas para una consultora ficticia llamada Iberbyte Ingeniería SL. La base de datos iberbyte_rrhh contiene tablas empleados, departamentos y facturas.
Instalación del módulo mysql2
Para comenzar a trabajar con MySQL en Node.js, instala el módulo mysql2 con npm:
npm install mysql2
Una vez instalado, puedes importar el módulo en tu aplicación Node.js y comenzar a establecer conexiones con tu base de datos MySQL. Durante todo el tutorial se utiliza la API basada en promesas (mysql2/promise), preferida en aplicaciones modernas.
Configuración básica de conexión
La configuración de conexión requiere especificar los parámetros necesarios para establecer comunicación con el servidor MySQL. Nunca incluyas credenciales en el código: lee siempre los valores desde variables de entorno.
// Configuración leída de variables de entorno (cargadas con --env-file=.env)
const connectionConfig = {
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT) || 3306,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME || 'iberbyte_rrhh'
};
Establecimiento de conexión simple
Para crear una conexión individual, utiliza el método createConnection() en su versión asíncrona:
import { createConnection } from 'mysql2/promise';
const conexion = await createConnection({
host: process.env.DB_HOST || 'localhost',
port: 3306,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: 'iberbyte_rrhh'
});
console.log('Conectado a MySQL con ID:', conexion.threadId);
Manejo de errores en conexiones
El manejo de errores es fundamental para crear aplicaciones sólidas. Los errores más comunes incluyen credenciales incorrectas, servidor no disponible o base de datos inexistente:
import { createConnection } from 'mysql2/promise';
async function conectar() {
try {
const conexion = await createConnection({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME
});
return conexion;
} catch (error) {
switch (error.code) {
case 'ER_ACCESS_DENIED_ERROR':
console.error('Error: credenciales de acceso incorrectas');
break;
case 'ECONNREFUSED':
console.error('Error: no se puede conectar al servidor MySQL');
break;
case 'ER_BAD_DB_ERROR':
console.error('Error: la base de datos no existe');
break;
default:
console.error('Error de conexión:', error.message);
}
throw error;
}
}
Pool de conexiones
Para aplicaciones que requieren múltiples conexiones simultáneas, el uso de un pool de conexiones es más eficiente que crear conexiones individuales. Un pool reutiliza conexiones existentes y limita la cantidad máxima para no saturar al servidor de base de datos.
import { createPool } from 'mysql2/promise';
// Crear pool de conexiones para producción
const pool = createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
connectionLimit: 20, // tamaño recomendado para una API con tráfico medio
queueLimit: 0, // sin límite en la cola de espera
waitForConnections: true,
enableKeepAlive: true,
keepAliveInitialDelay: 10_000
});
// Uso del pool: no necesitas liberar manualmente con execute()
const [empleados] = await pool.execute('SELECT id, nombre FROM empleados LIMIT 10');
console.log('Empleados obtenidos:', empleados.length);
Concepto clave: si usas
pool.execute(...)opool.query(...)directamente, el pool obtiene y libera la conexión automáticamente. Solo necesitas llamar aconnection.release()cuando obtienes una conexión explícita conpool.getConnection()(por ejemplo, para transacciones).
Configuración avanzada de conexión
Para entornos de producción, aplica parámetros adicionales de seguridad y rendimiento:
import { createPool } from 'mysql2/promise';
const pool = createPool({
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT) || 3306,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
charset: 'utf8mb4', // soporte completo UTF-8 (incluye emojis, acentos)
timezone: '+00:00', // UTC timezone en backend
ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: true } : undefined,
multipleStatements: false, // seguridad: evitar múltiples consultas en un solo string
connectionLimit: 20,
maxIdle: 10, // mantener máximo 10 conexiones inactivas
idleTimeout: 60_000, // cerrar conexiones inactivas tras 60 s
namedPlaceholders: true
});
Concepto clave:
rejectUnauthorized: trueen SSL valida el certificado del servidor MySQL. En producción es obligatorio para evitar ataques MITM. Nunca usesrejectUnauthorized: falseen servicios reales.
Cierre de conexiones
Al finalizar la aplicación, cierra el pool para liberar recursos del servidor MySQL. Esto es especialmente importante en entornos con graceful shutdown:
// Graceful shutdown al recibir SIGTERM o SIGINT
process.on('SIGTERM', async () => {
console.log('Cerrando pool de conexiones MySQL...');
await pool.end();
process.exit(0);
});
La gestión adecuada de conexiones es fundamental para el rendimiento y estabilidad de aplicaciones Node.js que interactúan con MySQL. El módulo mysql2 proporciona todas las herramientas necesarias para establecer conexiones seguras, eficientes y escalables.
Sentencias SELECT
Las consultas SELECT constituyen la base fundamental para recuperar información de bases de datos MySQL desde Node.js. Una vez establecido el pool con mysql2, puedes ejecutar consultas SQL para obtener datos específicos de tus tablas de manera eficiente y controlada.
Todas las consultas del tutorial usan sentencias parametrizadas con placeholders ?. Esta es la única defensa fiable contra inyección SQL, y mysql2 lo soporta de forma nativa con execute().
import { createPool } from 'mysql2/promise';
const pool = createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: 'iberbyte_rrhh',
connectionLimit: 20
});
// Consulta SELECT básica
const [empleados] = await pool.execute(
'SELECT id, nombre, email, salario FROM empleados LIMIT 50'
);
console.log('Empleados encontrados:', empleados.length);
console.log(empleados);
Consultas con filtros WHERE
Las cláusulas WHERE permiten filtrar registros según criterios específicos. Es fundamental utilizar consultas parametrizadas para evitar inyecciones SQL:
// Consulta con parámetro único: buscar un empleado por ID
const empleadoId = 42;
const [filas] = await pool.execute(
'SELECT id, nombre, email, departamento_id FROM empleados WHERE id = ?',
[empleadoId]
);
if (filas.length > 0) {
console.log('Empleado encontrado:', filas[0]);
} else {
console.log('Empleado no encontrado');
}
Para múltiples parámetros, utiliza un array con los valores en el orden correspondiente a los marcadores de posición:
// Empleados con salario superior a un umbral en un departamento concreto
const salarioMinimo = 35_000;
const departamentoId = 3; // Ingeniería
const [empleados] = await pool.execute(
`SELECT id, nombre, salario
FROM empleados
WHERE salario >= ? AND departamento_id = ?
ORDER BY salario DESC`,
[salarioMinimo, departamentoId]
);
console.log(`Empleados del departamento ${departamentoId} con salario >= ${salarioMinimo} EUR:`);
empleados.forEach((empleado) => {
console.log(`- ${empleado.nombre}: ${empleado.salario.toLocaleString('es-ES')} EUR`);
});
Consultas con async/await
El uso de async/await con el pool y promesas nativas produce código limpio y fácil de razonar:
import { createPool } from 'mysql2/promise';
const pool = createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: 'iberbyte_rrhh',
connectionLimit: 20
});
async function obtenerEmpleadosActivos() {
try {
const [rows] = await pool.execute(
'SELECT id, nombre, email FROM empleados WHERE activo = ?',
[1]
);
console.log('Empleados activos:', rows.length);
return rows;
} catch (error) {
console.error('Error en consulta:', error.message);
throw error;
}
}
const activos = await obtenerEmpleadosActivos();
activos.forEach((e) => console.log(`${e.nombre} <${e.email}>`));
Consultas con ORDER BY y LIMIT
Para ordenar resultados y limitar la cantidad de registros devueltos, utiliza las cláusulas ORDER BY y LIMIT:
async function topEmpleadosPorSalario(limite = 5) {
const [empleados] = await pool.execute(
'SELECT nombre, salario FROM empleados ORDER BY salario DESC LIMIT ?',
[limite]
);
console.log(`Top ${limite} empleados por salario:`);
empleados.forEach((empleado, index) => {
console.log(`${index + 1}. ${empleado.nombre}: ${empleado.salario} EUR`);
});
return empleados;
}
Consultas con JOIN
Las consultas JOIN permiten combinar datos de múltiples tablas relacionadas. El ejemplo siguiente devuelve empleados junto al nombre del departamento al que pertenecen:
async function empleadosConDepartamento(salarioMin, salarioMax) {
const [resultados] = await pool.execute(
`SELECT
e.id,
e.nombre AS empleado,
e.salario,
d.nombre AS departamento
FROM empleados e
INNER JOIN departamentos d ON e.departamento_id = d.id
WHERE e.salario BETWEEN ? AND ?
ORDER BY e.salario ASC`,
[salarioMin, salarioMax]
);
console.log('Empleados con departamento:');
resultados.forEach((fila) => {
console.log(`${fila.empleado} (${fila.departamento}): ${fila.salario} EUR`);
});
return resultados;
}
Consultas con funciones agregadas
Las funciones agregadas como COUNT, SUM, AVG proporcionan información estadística sobre los datos. Un caso habitual en software empresarial es calcular totales de facturación por trimestre:
async function estadisticasFacturacion(anio) {
const [filas] = await pool.execute(
`SELECT
COUNT(*) AS total_facturas,
AVG(total) AS ticket_medio,
MIN(total) AS factura_minima,
MAX(total) AS factura_maxima,
SUM(total) AS facturacion_total
FROM facturas
WHERE YEAR(fecha_emision) = ?`,
[anio]
);
const stats = filas[0];
console.log(`Estadísticas de facturación de ${anio}:`);
console.log(`- Total facturas: ${stats.total_facturas}`);
console.log(`- Ticket medio: ${Number(stats.ticket_medio).toFixed(2)} EUR`);
console.log(`- Rango: ${stats.factura_minima} EUR - ${stats.factura_maxima} EUR`);
console.log(`- Facturación total: ${Number(stats.facturacion_total).toFixed(2)} EUR`);
return stats;
}
Manejo de resultados vacíos
Es importante validar los resultados de las consultas para manejar casos donde no se encuentren registros. Un patrón común en endpoints REST es devolver 404 cuando no existe el recurso:
async function buscarEmpleadoPorEmail(email) {
const [empleados] = await pool.execute(
'SELECT id, nombre, email, departamento_id FROM empleados WHERE email = ?',
[email]
);
if (empleados.length === 0) {
console.log(`No se encontraron empleados con email "${email}"`);
return null;
}
return empleados[0];
}
// Uso
const empleado = await buscarEmpleadoPorEmail('maria.gonzalez@iberbyte.es');
if (empleado) {
console.log(`Encontrado: ${empleado.nombre} (ID ${empleado.id})`);
}
Consultas simultáneas con pool
Una de las ventajas del pool es que permite ejecutar varias consultas en paralelo reutilizando conexiones. Combinado con Promise.all se reducen los tiempos de respuesta de endpoints que necesitan datos de varias tablas:
async function obtenerEmpleadosPorDepartamento(departamentoId) {
const [empleados] = await pool.execute(
'SELECT id, nombre, salario FROM empleados WHERE departamento_id = ?',
[departamentoId]
);
return empleados;
}
// Múltiples consultas simultáneas
async function cuadroMandoDepartamentos() {
const [ingenieria, ventas, operaciones] = await Promise.all([
obtenerEmpleadosPorDepartamento(3),
obtenerEmpleadosPorDepartamento(5),
obtenerEmpleadosPorDepartamento(7)
]);
console.log('Ingeniería:', ingenieria.length, 'empleados');
console.log('Ventas:', ventas.length, 'empleados');
console.log('Operaciones:', operaciones.length, 'empleados');
}
Las consultas SELECT con mysql2 proporcionan flexibilidad completa para recuperar datos de MySQL desde Node.js. La combinación de consultas parametrizadas, promesas y pools de conexiones garantiza aplicaciones seguras, eficientes y escalables.
Sentencias INSERT
Las sentencias INSERT permiten agregar nuevos registros a las tablas de MySQL desde Node.js. Después de dominar las consultas SELECT, el siguiente paso natural es aprender a insertar datos de manera segura y eficiente utilizando el módulo mysql2.
La sintaxis básica de una sentencia INSERT utiliza el método execute() con placeholders parametrizados:
import { createPool } from 'mysql2/promise';
const pool = createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: 'iberbyte_rrhh',
connectionLimit: 20
});
// INSERT básico
const [resultado] = await pool.execute(
'INSERT INTO empleados (nombre, email, salario, departamento_id) VALUES (?, ?, ?, ?)',
['María González', 'maria.gonzalez@iberbyte.es', 42_500, 3]
);
console.log('Empleado insertado con ID:', resultado.insertId);
console.log('Filas afectadas:', resultado.affectedRows);
Inserción con async/await
El uso de async/await facilita el manejo de operaciones INSERT y permite un mejor control de errores:
async function crearEmpleado(nombre, email, salario, departamentoId) {
try {
const [result] = await pool.execute(
'INSERT INTO empleados (nombre, email, salario, departamento_id) VALUES (?, ?, ?, ?)',
[nombre, email, salario, departamentoId]
);
console.log(`Empleado "${nombre}" insertado con ID: ${result.insertId}`);
return result.insertId;
} catch (error) {
console.error('Error en inserción:', error.message);
throw error;
}
}
// Uso de la función
await crearEmpleado('Carlos Fernández', 'carlos.fernandez@iberbyte.es', 38_000, 5);
Inserción múltiple de registros
Para insertar varios registros de manera eficiente en una sola operación, utiliza pool.query() con VALUES múltiples (la versión query admite listas anidadas mientras que execute solo admite placeholders simples):
async function crearEmpleadosEnLote(empleados) {
// Preparar valores para inserción múltiple
const valores = empleados.map((e) => [e.nombre, e.email, e.salario, e.departamentoId]);
const [result] = await pool.query(
'INSERT INTO empleados (nombre, email, salario, departamento_id) VALUES ?',
[valores]
);
console.log(`${result.affectedRows} empleados insertados`);
console.log('Primer ID insertado:', result.insertId);
return result;
}
// Datos de ejemplo: nuevo departamento de ingeniería
const nuevosEmpleados = [
{ nombre: 'Ana Martín', email: 'ana.martin@iberbyte.es', salario: 41_000, departamentoId: 3 },
{ nombre: 'Javier Ruiz', email: 'javier.ruiz@iberbyte.es', salario: 38_500, departamentoId: 3 },
{ nombre: 'Lucía Navarro', email: 'lucia.navarro@iberbyte.es', salario: 45_000, departamentoId: 3 }
];
await crearEmpleadosEnLote(nuevosEmpleados);
Validación de datos antes de insertar
Es fundamental validar los datos antes de realizar inserciones para mantener la integridad de la base de datos. En un entorno profesional conviene centralizar la validación con una librería como Zod (tratada en la lección de validación), aunque aquí mostramos un patrón manual:
async function crearEmpleadoValidado(datos) {
// Validaciones básicas
if (!datos.nombre || datos.nombre.trim().length === 0) {
throw new TypeError('El nombre del empleado es obligatorio');
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(datos.email)) {
throw new TypeError('Formato de email no válido');
}
if (typeof datos.salario !== 'number' || datos.salario <= 0) {
throw new RangeError('El salario debe ser un número mayor que cero');
}
try {
const [result] = await pool.execute(
`INSERT INTO empleados (nombre, email, salario, departamento_id, fecha_alta)
VALUES (?, ?, ?, ?, NOW())`,
[
datos.nombre.trim(),
datos.email.toLowerCase(),
datos.salario,
datos.departamentoId
]
);
console.log('Empleado validado e insertado:', result.insertId);
return result.insertId;
} catch (error) {
if (error.code === 'ER_DUP_ENTRY') {
throw new Error('Ya existe un empleado con ese email');
}
throw error;
}
}
// Ejemplo de uso con validación
const nuevo = {
nombre: 'Pablo Serrano',
email: 'pablo.serrano@iberbyte.es',
salario: 39_500,
departamentoId: 5
};
try {
const id = await crearEmpleadoValidado(nuevo);
console.log('Empleado creado con ID', id);
} catch (error) {
console.error('Error de validación:', error.message);
}
Inserción con campos opcionales
Para manejar campos opcionales en las inserciones, construye dinámicamente la consulta SQL según los datos disponibles. Es habitual en endpoints POST que admiten payloads parciales:
async function crearEmpleadoFlexible(datos) {
// Construir campos y valores dinámicamente
const campos = ['nombre', 'email'];
const valores = [datos.nombre, datos.email];
const placeholders = ['?', '?'];
// Agregar campos opcionales si están presentes
if (datos.telefono) {
campos.push('telefono');
valores.push(datos.telefono);
placeholders.push('?');
}
if (datos.salario !== undefined) {
campos.push('salario');
valores.push(datos.salario);
placeholders.push('?');
}
if (datos.departamentoId) {
campos.push('departamento_id');
valores.push(datos.departamentoId);
placeholders.push('?');
}
// Construir consulta SQL (los nombres de campo se controlan desde el código, no desde el usuario)
const sql = `INSERT INTO empleados (${campos.join(', ')}) VALUES (${placeholders.join(', ')})`;
const [result] = await pool.execute(sql, valores);
console.log('Empleado insertado con campos flexibles:', result.insertId);
return result.insertId;
}
// Ejemplo: alta solo con campos mínimos
await crearEmpleadoFlexible({
nombre: 'Elena Ortega',
email: 'elena.ortega@iberbyte.es',
telefono: '+34 612 345 678'
});
Concepto clave: los nombres de campo nunca deben venir del usuario final. La construcción dinámica es segura solo si las claves (
campos) están controladas por el servidor. Los valores, en cambio, siempre se pasan por parámetros.
Inserción con pool de conexiones
Para aplicaciones con múltiples inserciones concurrentes, el pool compartido reutiliza conexiones de forma eficiente sin necesidad de gestionar ciclo de vida manualmente:
import { createPool } from 'mysql2/promise';
const pool = createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: 'iberbyte_rrhh',
connectionLimit: 20
});
async function crearFactura(clienteId, total, concepto) {
const [result] = await pool.execute(
`INSERT INTO facturas (cliente_id, total, concepto, fecha_emision)
VALUES (?, ?, ?, NOW())`,
[clienteId, total, concepto]
);
return result.insertId;
}
// Procesar un lote de facturas emitidas hoy
async function emitirLoteFacturas(facturas) {
const ids = await Promise.all(
facturas.map((f) => crearFactura(f.clienteId, f.total, f.concepto))
);
console.log('Facturas emitidas:', ids);
return ids;
}
Manejo de errores específicos en INSERT
Las inserciones pueden fallar por motivos diversos (clave duplicada, constraint violado, campo obligatorio vacío). Un patrón útil es traducir los códigos de error de MySQL a mensajes claros para el cliente de la API:
async function insertarConManejoErrores(datos) {
try {
const [result] = await pool.execute(
'INSERT INTO empleados (nombre, email, salario, departamento_id) VALUES (?, ?, ?, ?)',
[datos.nombre, datos.email, datos.salario, datos.departamentoId]
);
return {
exito: true,
id: result.insertId,
mensaje: 'Empleado insertado correctamente'
};
} catch (error) {
let mensajeError;
switch (error.code) {
case 'ER_DUP_ENTRY':
mensajeError = 'Ya existe un empleado con ese email';
break;
case 'ER_NO_REFERENCED_ROW_2':
mensajeError = 'El departamento especificado no existe';
break;
case 'ER_BAD_NULL_ERROR':
mensajeError = 'Faltan campos obligatorios';
break;
case 'ER_DATA_TOO_LONG':
mensajeError = 'Algún campo excede la longitud máxima permitida';
break;
default:
mensajeError = `Error de base de datos: ${error.message}`;
}
return {
exito: false,
error: error.code,
mensaje: mensajeError
};
}
}
// Uso con manejo completo de errores
const empleado = {
nombre: 'Ricardo Molina',
email: 'ricardo.molina@iberbyte.es',
salario: 40_000,
departamentoId: 3
};
const resultado = await insertarConManejoErrores(empleado);
if (resultado.exito) {
console.log('OK -', resultado.mensaje, '- ID:', resultado.id);
} else {
console.error('KO -', resultado.mensaje);
}
Las sentencias INSERT con mysql2 proporcionan todas las herramientas necesarias para agregar datos de manera segura y eficiente a bases de datos MySQL desde Node.js. La combinación de consultas parametrizadas, validación de datos, variables de entorno para las credenciales y manejo apropiado de errores garantiza la integridad y confiabilidad de las operaciones de inserción en aplicaciones empresariales.
Fuentes y referencias
Documentación oficial y recursos externos para profundizar en Node.js
Documentación oficial de Node.js
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Node.js es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Node.js
Explora más contenido relacionado con Node.js y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Comprender cómo establecer conexiones a bases de datos MySQL usando el módulo mysql2 en Node.js. Aprender a ejecutar consultas SELECT con filtros, joins y funciones agregadas, manejando resultados y errores. Conocer la inserción de datos mediante sentencias INSERT, incluyendo inserciones múltiples y validación de datos. Utilizar promesas y async/await para un manejo asíncrono eficiente de las operaciones con mysql2. Implementar pools de conexiones para optimizar el rendimiento en aplicaciones con múltiples consultas simultáneas.