INSERT en Node.js

Insertar datos es una de las operaciones fundamentales en cualquier aplicación que trabaje con bases de datos. En Node.js con mysql2, puedes insertar registros individuales o múltiples de forma eficiente utilizando sentencias preparadas que protegen contra inyecciones SQL. En este artículo aprenderás las diferentes técnicas para insertar datos en MySQL desde tu aplicación Node.js.

Requisitos previos

Necesitas tener configurado un pool de conexiones como se describió en los artículos anteriores. Utilizaremos la misma tabla productos de la base de datos tienda. Si no la tienes, créala con el siguiente SQL:

USE tienda;
 
CREATE TABLE IF NOT EXISTS productos (
    id INT AUTO_INCREMENT PRIMARY KEY,
    nombre VARCHAR(100) NOT NULL,
    categoria VARCHAR(50) NOT NULL,
    precio DECIMAL(10, 2) NOT NULL,
    stock INT DEFAULT 0,
    activo TINYINT(1) DEFAULT 1,
    fecha_creacion DATETIME DEFAULT CURRENT_TIMESTAMP
);

Código completo

Este ejemplo inserta un producto y muestra el ID generado automáticamente:

const mysql = require('mysql2/promise');
 
async function insertarProducto() {
  const pool = mysql.createPool({
    host: 'localhost',
    user: 'root',
    password: 'tu_contraseña',
    database: 'tienda'
  });
 
  const [result] = await pool.execute(
    'INSERT INTO productos (nombre, categoria, precio, stock) VALUES (?, ?, ?, ?)',
    ['Tablet Samsung Galaxy Tab S9', 'Tablets', 8999.00, 35]
  );
 
  console.log('Producto insertado exitosamente');
  console.log('ID generado:', result.insertId);
  console.log('Filas afectadas:', result.affectedRows);
 
  await pool.end();
}
 
insertarProducto().catch(console.error);

Salida esperada:

Producto insertado exitosamente
ID generado: 9
Filas afectadas: 1

Explicación paso a paso

Cuando ejecutas una sentencia INSERT con execute(), el resultado no contiene filas de datos como en un SELECT, sino un objeto ResultSetHeader con información sobre la operación realizada. Las propiedades más importantes de este objeto son insertId, que contiene el valor del campo AUTO_INCREMENT generado, y affectedRows, que indica cuántas filas se insertaron.

Los valores se pasan como un array de parámetros que se sustituyen en los placeholders ? de la consulta. El paquete mysql2 se encarga de escapar correctamente cada valor según su tipo, convirtiendo strings en cadenas entrecomilladas, números en valores numéricos y null en NULL de SQL.

Insertar con datos de un objeto

En la práctica, los datos suelen venir de un formulario o una petición HTTP en forma de objeto. Puedes extraer los valores del objeto para pasarlos como parámetros:

async function crearProducto(producto) {
  const { nombre, categoria, precio, stock } = producto;
 
  const [result] = await pool.execute(
    'INSERT INTO productos (nombre, categoria, precio, stock) VALUES (?, ?, ?, ?)',
    [nombre, categoria, precio, stock || 0]
  );
 
  return { id: result.insertId, ...producto };
}
 
// Uso
const nuevo = await crearProducto({
  nombre: 'Cargador USB-C 65W',
  categoria: 'Accesorios',
  precio: 599.00,
  stock: 300
});
 
console.log('Producto creado:', nuevo);

Salida esperada:

Producto creado: { id: 10, nombre: 'Cargador USB-C 65W', categoria: 'Accesorios', precio: 599, stock: 300 }

Inserción masiva (bulk insert)

Cuando necesitas insertar muchos registros, es mucho más eficiente hacerlo en una sola consulta. El paquete mysql2 permite usar query() con la sintaxis de múltiples valores. Para inserciones masivas se recomienda usar query() en lugar de execute() porque las sentencias preparadas no soportan directamente la expansión de arrays:

async function insertarMultiples() {
  const productos = [
    ['Impresora HP LaserJet', 'Impresoras', 3499.00, 20],
    ['Cable HDMI 2.1 3m', 'Accesorios', 349.00, 500],
    ['Hub USB-C 7 en 1', 'Accesorios', 899.00, 150],
    ['Mousepad XXL Gaming', 'Accesorios', 449.00, 200],
    ['Soporte Monitor Ajustable', 'Accesorios', 1299.00, 75]
  ];
 
  const [result] = await pool.query(
    'INSERT INTO productos (nombre, categoria, precio, stock) VALUES ?',
    [productos]
  );
 
  console.log('Registros insertados:', result.affectedRows);
  console.log('Primer ID generado:', result.insertId);
}
 
insertarMultiples().catch(console.error);

Salida esperada:

Registros insertados: 5
Primer ID generado: 11

Observa que en la inserción masiva se usa un solo placeholder ? después de VALUES, y los datos se envuelven en un array adicional. El insertId devuelto corresponde al primer registro insertado; los demás tendrán IDs consecutivos.

INSERT ... ON DUPLICATE KEY UPDATE

Esta variante es muy útil cuando quieres insertar un registro pero, si ya existe un registro con la misma clave primaria o índice único, actualizar ciertos campos en lugar de provocar un error:

async function insertarOActualizar(producto) {
  const { nombre, categoria, precio, stock } = producto;
 
  const [result] = await pool.execute(
    `INSERT INTO productos (nombre, categoria, precio, stock)
     VALUES (?, ?, ?, ?)
     ON DUPLICATE KEY UPDATE
       precio = VALUES(precio),
       stock = stock + VALUES(stock)`,
    [nombre, categoria, precio, stock]
  );
 
  // affectedRows: 1 = insertado, 2 = actualizado
  if (result.affectedRows === 1) {
    console.log('Producto insertado con ID:', result.insertId);
  } else if (result.affectedRows === 2) {
    console.log('Producto actualizado. Stock incrementado.');
  } else {
    console.log('Sin cambios (el registro ya existía con los mismos valores).');
  }
 
  return result;
}

Cuando se actualiza un registro existente, MySQL reporta affectedRows como 2 (uno por el intento de inserción y otro por la actualización). Si el registro existía pero los valores no cambiaron, affectedRows será 0.

Caso práctico

Veamos un ejemplo realista de un endpoint de API que recibe datos de un formulario y los inserta en la base de datos con validación:

const mysql = require('mysql2/promise');
 
const pool = mysql.createPool({
  host: 'localhost',
  user: 'root',
  password: 'tu_contraseña',
  database: 'tienda'
});
 
async function registrarPedido(clienteId, items) {
  const connection = await pool.getConnection();
 
  try {
    await connection.beginTransaction();
 
    // Insertar el pedido
    const [pedido] = await connection.execute(
      'INSERT INTO pedidos (cliente_id, fecha, estado) VALUES (?, NOW(), ?)',
      [clienteId, 'pendiente']
    );
    const pedidoId = pedido.insertId;
 
    // Insertar cada línea del pedido
    for (const item of items) {
      await connection.execute(
        'INSERT INTO detalle_pedido (pedido_id, producto_id, cantidad, precio_unitario) VALUES (?, ?, ?, ?)',
        [pedidoId, item.productoId, item.cantidad, item.precioUnitario]
      );
 
      // Descontar del stock
      await connection.execute(
        'UPDATE productos SET stock = stock - ? WHERE id = ? AND stock >= ?',
        [item.cantidad, item.productoId, item.cantidad]
      );
    }
 
    await connection.commit();
    console.log(`Pedido #${pedidoId} registrado con ${items.length} productos`);
    return pedidoId;
 
  } catch (error) {
    await connection.rollback();
    throw error;
  } finally {
    connection.release();
  }
}
 
// Uso
registrarPedido(42, [
  { productoId: 1, cantidad: 1, precioUnitario: 12999.99 },
  { productoId: 2, cantidad: 2, precioUnitario: 1599.00 }
]).catch(console.error);

Salida esperada:

Pedido #1 registrado con 2 productos

Manejo de errores

Los errores más comunes al insertar datos son las violaciones de restricciones. Es importante capturarlos y dar mensajes claros al usuario:

async function insertarConManejo(nombre, categoria, precio, stock) {
  try {
    const [result] = await pool.execute(
      'INSERT INTO productos (nombre, categoria, precio, stock) VALUES (?, ?, ?, ?)',
      [nombre, categoria, precio, stock]
    );
    return { exito: true, id: result.insertId };
 
  } catch (error) {
    switch (error.code) {
      case 'ER_DUP_ENTRY':
        return { exito: false, mensaje: 'Ya existe un producto con ese nombre' };
      case 'ER_BAD_NULL_ERROR':
        return { exito: false, mensaje: `El campo ${error.sqlMessage} es obligatorio` };
      case 'ER_DATA_TOO_LONG':
        return { exito: false, mensaje: 'Uno de los valores excede la longitud permitida' };
      case 'ER_TRUNCATED_WRONG_VALUE_FOR_FIELD':
        return { exito: false, mensaje: 'Tipo de dato incorrecto para uno de los campos' };
      case 'ER_NO_REFERENCED_ROW_2':
        return { exito: false, mensaje: 'La referencia a otra tabla no es válida' };
      default:
        console.error('Error al insertar:', error);
        return { exito: false, mensaje: 'Error interno al guardar el producto' };
    }
  }
}

Ahora que sabes cómo insertar datos, en el siguiente artículo aprenderás a actualizar registros existentes en MySQL desde Node.js.

Escrito por Eduardo Lázaro