Saltar al contenido.

Blog

Publicado el 24/07/2026 | Autor: Enrique Stolar

CRUD con MySQL desde Node.js

CRUD con MySQL desde Node.js

Serie: Desarrollo de Interfaces

Curso: Desarrollo de Interfaces 1

Capítulo 21: CRUD con MySQL desde Node.js

Capítulo anterior: Crear una base de datos y tablas en MySQL

Capítulo siguiente: Consultas preparadas, seguridad y validación en backend

Ya tenemos la ruta completa para llegar hasta aquí. Primero trabajamos datos en memoria, luego persistencia con JSON, después consumo de APIs, bases de datos relacionales y creación de tablas en MySQL. Ahora sí: conectaremos Node.js con MySQL para construir un CRUD real.

La idea principal es mantener el contrato que la interfaz ya entiende: rutas HTTP que devuelven JSON. Lo que cambia está por dentro del servidor. En vez de leer y escribir un archivo, ejecutaremos consultas SQL sobre una base de datos.

Qué aprenderás hoy

  • Conectar Node.js con MySQL usando mysql2/promise.
  • Crear un pool de conexiones.
  • Construir rutas CRUD con Express.
  • Usar consultas preparadas con parámetros.
  • Devolver respuestas JSON útiles para una interfaz.
  • Separar mínimamente conexión, consultas y rutas.
Idea clave

La interfaz no necesita saber si el backend guarda datos en JSON o MySQL. Si el contrato de la API se mantiene, podemos mejorar la persistencia sin rehacer la pantalla.

Estructura del proyecto

Usaremos una estructura simple, suficiente para aprender sin perdernos en arquitectura.

crud-mysql-node/
  src/
    db.js
    server.js
  .env
  package.json

db.js tendrá la conexión. server.js tendrá las rutas. Más adelante podremos separar modelos, servicios y controladores, pero hoy lo mantendremos claro y directo.

Instalar dependencias

Necesitamos Express para crear la API, MySQL2 para conectarnos a MySQL y dotenv para leer variables de entorno desde un archivo .env.

npm init -y
npm install express mysql2 dotenv

Luego podemos ajustar package.json para usar módulos modernos:

{
  "type": "module",
  "scripts": {
    "start": "node src/server.js"
  },
  "dependencies": {
    "dotenv": "^16.0.0",
    "express": "^4.18.0",
    "mysql2": "^3.0.0"
  }
}

Configurar variables de entorno

La conexión a la base de datos no debería estar escrita directamente dentro del código. Es mejor usar variables de entorno.

DB_HOST=localhost
DB_USER=root
DB_PASSWORD=
DB_NAME=di_projects
PORT=3000

En tu laptop puede que el usuario sea root sin contraseña. En hosting o producción, normalmente tendrás un usuario y contraseña específicos. La idea es que el código no cambie: cambian las variables.

Cuidado

No subas contraseñas reales al repositorio. En proyectos reales, .env debe estar en .gitignore.

Crear el pool de conexiones

Un pool administra varias conexiones reutilizables. En vez de abrir una conexión nueva por cada solicitud, el pool permite trabajar de forma más eficiente.

import 'dotenv/config';
import mysql from 'mysql2/promise';

export const pool = mysql.createPool({
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME,
  waitForConnections: true,
  connectionLimit: 10,
  queueLimit: 0
});

Este archivo no ejecuta consultas todavía. Solo prepara una conexión compartida para que el servidor pueda usarla.

Crear el servidor Express

Ahora configuramos Express y activamos express.json() para recibir datos enviados desde la interfaz.

import express from 'express';
import { pool } from './db.js';

const app = express();
const port = process.env.PORT || 3000;

app.use(express.json());

app.get('/', function (request, response) {
  response.json({ message: 'API de proyectos funcionando' });
});

app.listen(port, function () {
  console.log('Servidor iniciado en http://localhost:' + port);
});

Si ejecutas npm start, deberías tener una API disponible en http://localhost:3000.

Read: listar proyectos

La primera operación será listar proyectos junto con el nombre del curso. Usaremos INNER JOIN para combinar projects y courses.

app.get('/projects', async function (request, response) {
  const [rows] = await pool.execute(`
    SELECT
      projects.id,
      projects.title,
      projects.team_name,
      projects.status,
      courses.name AS course_name
    FROM projects
    INNER JOIN courses ON courses.id = projects.course_id
    ORDER BY projects.created_at DESC
  `);

  response.json(rows);
});

La interfaz recibirá un arreglo de objetos. Eso significa que puede seguir renderizando tarjetas, listas o tablas sin conocer los detalles internos de MySQL.

Read: obtener un proyecto por id

Para consultar un solo proyecto, usamos un parámetro de ruta. Aquí ya conviene usar una consulta preparada con ?.

app.get('/projects/:id', async function (request, response) {
  const id = Number(request.params.id);

  const [rows] = await pool.execute(
    `SELECT id, course_id, title, team_name, summary, status
     FROM projects
     WHERE id = ?`,
    [id]
  );

  if (rows.length === 0) {
    response.status(404).json({ message: 'Proyecto no encontrado' });
    return;
  }

  response.json(rows[0]);
});

El signo ? no es decoración. Representa un valor que MySQL2 enviará de forma parametrizada. Eso evita construir consultas pegando texto manualmente.

Create: registrar un proyecto

Para crear un proyecto recibimos datos desde el cuerpo de la solicitud. Usaremos POST /projects.

app.post('/projects', async function (request, response) {
  const { course_id, title, team_name, summary, status } = request.body;

  const [result] = await pool.execute(
    `INSERT INTO projects (course_id, title, team_name, summary, status)
     VALUES (?, ?, ?, ?, ?)`,
    [course_id, title, team_name, summary || null, status || 'draft']
  );

  response.status(201).json({
    id: result.insertId,
    course_id,
    title,
    team_name,
    summary,
    status: status || 'draft'
  });
});

result.insertId contiene el identificador generado por MySQL. La interfaz puede usarlo si necesita mostrar el nuevo registro o redirigir a una pantalla de detalle.

Update: modificar un proyecto

Para actualizar usamos PUT /projects/:id. La consulta cambia campos específicos y registra una fecha de actualización.

app.put('/projects/:id', async function (request, response) {
  const id = Number(request.params.id);
  const { course_id, title, team_name, summary, status } = request.body;

  const [result] = await pool.execute(
    `UPDATE projects
     SET course_id = ?,
         title = ?,
         team_name = ?,
         summary = ?,
         status = ?,
         updated_at = NOW()
     WHERE id = ?`,
    [course_id, title, team_name, summary || null, status, id]
  );

  if (result.affectedRows === 0) {
    response.status(404).json({ message: 'Proyecto no encontrado' });
    return;
  }

  response.json({ message: 'Proyecto actualizado' });
});

affectedRows nos indica si realmente se modificó alguna fila. Si es cero, probablemente el id no existe.

Delete: eliminar un proyecto

Para eliminar, usamos DELETE /projects/:id. Igual que antes, debemos revisar si la operación afectó una fila.

app.delete('/projects/:id', async function (request, response) {
  const id = Number(request.params.id);

  const [result] = await pool.execute(
    'DELETE FROM projects WHERE id = ?',
    [id]
  );

  if (result.affectedRows === 0) {
    response.status(404).json({ message: 'Proyecto no encontrado' });
    return;
  }

  response.json({ message: 'Proyecto eliminado' });
});

En una aplicación real, a veces no eliminamos físicamente registros. Podemos usar un campo como deleted_at o active. Pero para aprender CRUD, esta versión directa es suficiente.

Manejo básico de errores

Si algo falla en la consulta, no queremos que la API se quede sin responder. Una forma inicial es envolver cada ruta con try/catch.

app.get('/courses', async function (request, response) {
  try {
    const [rows] = await pool.execute(
      'SELECT id, name, code FROM courses ORDER BY name'
    );

    response.json(rows);
  } catch (error) {
    console.error(error);
    response.status(500).json({ message: 'Error al consultar cursos' });
  }
});

Más adelante podemos crear un middleware central de errores. Hoy basta con entender la idea: la API debe responder de forma clara incluso cuando algo sale mal.

Separar responsabilidades

Si todas las consultas viven dentro de server.js, el archivo crece rápido. Una mejora natural es separar funciones de acceso a datos.

export async function findAllProjects(pool) {
  const [rows] = await pool.execute(`
    SELECT projects.id, projects.title, projects.team_name,
           projects.status, courses.name AS course_name
    FROM projects
    INNER JOIN courses ON courses.id = projects.course_id
    ORDER BY projects.created_at DESC
  `);

  return rows;
}

Después la ruta solo llama a la función:

app.get('/projects', async function (request, response) {
  const projects = await findAllProjects(pool);
  response.json(projects);
});

Este pequeño cambio prepara el terreno para hablar de capas, servicios y POO más adelante. No usamos clases todavía, pero ya estamos separando responsabilidades.

Errores comunes

  • Escribir usuario y contraseña directamente en el código.
  • Construir SQL concatenando valores recibidos desde la interfaz.
  • No revisar affectedRows en operaciones de actualización o eliminación.
  • No devolver estados HTTP adecuados como 201, 404 o 500.
  • Mezclar toda la lógica en un solo archivo sin separar conexión, rutas y consultas.
  • Responder solo con mensajes genéricos que no ayudan a la interfaz.
Recomendación profesional

Primero haz que el CRUD funcione. Luego refactoriza. Aprender backend también consiste en mejorar la estructura cuando ya entiendes el flujo completo.

Reto práctico

Reto

Construye una API CRUD para la tabla projects usando Node.js, Express y MySQL. Debe listar, consultar por id, crear, actualizar y eliminar proyectos. Todas las consultas con datos externos deben usar parámetros.

Variante extra: conecta esta API con una interfaz HTML usando fetch(). La pantalla debe mostrar estados de carga, error, lista vacía y operación exitosa.

Conclusión

Con este capítulo, el proyecto deja de ser un prototipo con datos sueltos y se convierte en una aplicación conectada a una base de datos real. La interfaz habla con una API. La API procesa solicitudes. MySQL guarda y consulta información estructurada.

Este es un punto muy importante para Desarrollo de Interfaces: una pantalla profesional no vive sola. Necesita datos, estados, validación, errores y persistencia. Entender el flujo completo ayuda a diseñar interfaces más realistas y a conversar mejor con equipos backend.

En el siguiente capítulo profundizaremos en consultas preparadas, seguridad y validación en backend. Ahí veremos cómo proteger mejor la API antes de pensar en organización más avanzada, servicios y POO.

Fuentes consultadas

Compartir este artículo: