Una API sin base de datos olvida todo en cuanto se reinicia. Node.js se lleva bien tanto con bases de datos relacionales como con bases de documentos, y en muchos proyectos reales conviven las dos. En el libro Node.js desde cero trabajamos con MySQL y con MongoDB; acá tenés cómo conectar cada una de forma correcta y el mismo CRUD escrito para ambas.
MySQL o MongoDB: cuál elegir
| MySQL (relacional) | MongoDB (documentos) | |
|---|---|---|
| Estructura | Tablas con columnas fijas | Documentos JSON con forma flexible |
| Relaciones | Claves foráneas y JOIN | Documentos anidados o referencias |
| Integridad | Fuerte: tipos, restricciones, transacciones | Más flexible; también admite transacciones |
| Ideal para | Pedidos, facturación, usuarios y permisos | Catálogos variables, contenido, logs, eventos |
Una regla práctica: si los datos tienen relaciones claras y la consistencia es crítica (dinero, stock), empezá por una base relacional. Si cada registro puede tener campos distintos y se lee casi siempre entero, MongoDB encaja muy bien.
MySQL con mysql2
$ npm install mysql2
import mysql from 'mysql2/promise';
// Un pool reutiliza conexiones: crealo UNA vez y compartilo en toda la app
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,
connectionLimit: 10,
});
export const postsMySQL = {
async listar() {
const [filas] = await pool.query('SELECT id, titulo, publicado FROM posts ORDER BY id DESC');
return filas;
},
async buscarPorId(id) {
// execute() usa sentencias preparadas: el valor viaja separado del SQL
const [filas] = await pool.execute('SELECT * FROM posts WHERE id = ?', [id]);
return filas[0] ?? null;
},
async crear({ titulo, contenido }) {
const [res] = await pool.execute(
'INSERT INTO posts (titulo, contenido) VALUES (?, ?)',
[titulo, contenido],
);
return { id: res.insertId, titulo, contenido };
},
async borrar(id) {
const [res] = await pool.execute('DELETE FROM posts WHERE id = ?', [id]);
return res.affectedRows > 0;
},
};
`... WHERE id = ${req.params.id}`): es la
puerta de entrada de la inyección SQL. Los marcadores ? resuelven el problema.
Transacciones
Cuando varias operaciones tienen que ocurrir juntas o ninguna, usá una conexión del pool y una transacción:
import { pool } from './mysql.js';
export async function crearPedido(usuarioId, productoId, cantidad) {
const conn = await pool.getConnection();
try {
await conn.beginTransaction();
const [r] = await conn.execute(
'UPDATE productos SET stock = stock - ? WHERE id = ? AND stock >= ?',
[cantidad, productoId, cantidad],
);
if (r.affectedRows === 0) throw new Error('Stock insuficiente');
await conn.execute(
'INSERT INTO pedidos (usuario_id, producto_id, cantidad) VALUES (?, ?, ?)',
[usuarioId, productoId, cantidad],
);
await conn.commit();
} catch (err) {
await conn.rollback();
throw err;
} finally {
conn.release(); // devolver la conexión al pool, pase lo que pase
}
}
MongoDB con el driver oficial
$ npm install mongodb
import { MongoClient, ObjectId } from 'mongodb';
// Un solo cliente para toda la aplicación: gestiona su propio pool interno
const client = new MongoClient(process.env.MONGODB_URI);
await client.connect();
const db = client.db('blog');
const coleccion = db.collection('posts');
// Índice para las búsquedas frecuentes (se crea una vez; si ya existe, no hace nada)
await coleccion.createIndex({ publicado: 1, creadoEn: -1 });
export const postsMongo = {
listar() {
return coleccion.find({ publicado: true }).sort({ creadoEn: -1 }).limit(20).toArray();
},
buscarPorId(id) {
if (!ObjectId.isValid(id)) return null;
return coleccion.findOne({ _id: new ObjectId(id) });
},
async crear({ titulo, contenido, etiquetas = [] }) {
const doc = { titulo, contenido, etiquetas, publicado: false, creadoEn: new Date() };
const { insertedId } = await coleccion.insertOne(doc);
return { _id: insertedId, ...doc };
},
async borrar(id) {
if (!ObjectId.isValid(id)) return false;
const { deletedCount } = await coleccion.deleteOne({ _id: new ObjectId(id) });
return deletedCount > 0;
},
};
export { client };
req.body directamente como
filtro, un atacante puede enviar { "password": { "$ne": null } }. Construí siempre los
filtros con valores validados, nunca con objetos enteros que vengan del cliente.
Si preferís trabajar con modelos y esquemas, Mongoose añade validación y una capa de modelos encima del driver; para empezar, el driver oficial es más directo y te enseña cómo funciona MongoDB de verdad.
Configuración con variables de entorno
DB_HOST=localhost
DB_USER=blog_app
DB_PASSWORD=una-clave-larga-y-unica
DB_NAME=blog
MONGODB_URI=mongodb://localhost:27017
Desde Node.js 20.6 podés cargarlo sin dependencias: node --env-file=.env src/server.js.
Añadí .env a .gitignore y documentá las variables en un
.env.example sin valores reales.
Errores comunes
- Crear una conexión nueva en cada petición: agota las conexiones de la base de datos bajo carga. Un pool (MySQL) o un cliente (MongoDB) para toda la aplicación.
- Olvidar
conn.release(): el pool se queda sin conexiones y la API se congela. - Devolver documentos completos: seleccioná solo los campos necesarios; nunca envíes el hash de la contraseña al cliente.
- No crear índices: funciona con 100 registros y se arrastra con 100.000.
- Usuario root en producción: creá un usuario de base de datos propio para la aplicación, con los permisos mínimos.
Para validar los datos antes de guardarlos, revisá validación y manejo de errores.
Datos relacionales y NoSQL en un mismo libro
El libro Node.js desde cero enseña a conectar aplicaciones a bases de datos relacionales y NoSQL, MySQL y MongoDB, y a usarlas en sus proyectos.
Comprar en Amazon