Base de Datos - Persistencia Relacional con SQLite y SQL
🚀 Clases en Vivo — Inicio: Domingo 6 de Septiembre ($200 MXN / clase). Aprende a programar desde cero con acompañamiento directo y construye el proyecto ShortURL.
En la Lección 7: Backend - Servidor Web y API REST con Node.js (L7) construimos el servidor web de ShortURL, implementamos endpoints POST y GET, y conectamos la interfaz gráfica mediante fetch().
Nuestra aplicación ya es capaz de recibir URLs, generar códigos cortos y redirigir usuarios en milisegundos. Sin embargo, tenemos un problema crítico: cada vez que apagamos la computadora o reiniciamos el servidor con Ctrl + C, ¡todos los enlaces guardados en memoria desaparecen por completo!
En esta sesión intensiva de 4 horas, resolveremos este problema de raíz implementando la tercera y última gran capa de nuestro stack: la Base de Datos. Aprenderemos los fundamentos de las bases de datos relacionales, dominaremos el lenguaje universal SQL (Structured Query Language) e integraremos SQLite en Node.js para que nuestros enlaces y estadísticas queden guardados para siempre.
🗺️ Estructura de la Sesión
Cada una de nuestras sesiones semanales sigue una metodología pedagógica dividida en 5 fases:
Estructura de la Sesión├── ☕ Fase 1: Revisión del Tema Anterior y Conclusiones├── 🎯 Fase 2: Introducción al Tema y Fundamentos├── 💻 Fase 3: Práctica Asistida en Vivo├── 🗣️ Fase 4: Exposición, Debate y Debugging└── 🏠 Fase 5: Reto Semanal y Práctica en Casa☕ Fase 1: Revisión del Tema Anterior y Conclusiones
Iniciamos la sesión analizando las limitaciones del almacenamiento en memoria RAM y conectando con la necesidad de persistencia.
1. Puesta en Común del Reto de la Lección 7
En la lección previa construimos los endpoints POST /api/acortar, GET /:codigo y el endpoint de métricas:
- Discusión en grupo: ¿Pudiste acortar un enlace real y comprobar la redirección hacia el sitio de destino?
- ¿Qué ocurrió cuando detuviste el servidor Node.js en la terminal y lo volviste a encender?
- ¿Por qué una empresa como Bitly, YouTube o Twitter no podría operar guardando sus datos en un arreglo
const baseDeDatos = []en memoria?
2. El Puente Conceptual: La Bóveda Permanente de Datos
La memoria RAM es ultra rápida, pero es volátil: requiere electricidad constante para recordar los datos. Un disco duro o unidad SSD, por el contrario, es persistente: guarda la información magnética o electrónicamente para que sobreviva a reinicios y apagones.
Una Base de Datos es un sistema especializado diseñado para:
- Guardar millones de datos en el disco de forma estructurada.
- Buscar registros específicos en fracciones de milisegundo mediante índices.
- Garantizar que ninguna información se corrompa si ocurren errores o caídas del sistema.
🎯 Fase 2: Introducción al Tema y Fundamentos Teóricos
Analicemos la estructura de las bases de datos relacionales, el lenguaje SQL y por qué SQLite es la herramienta ideal.
1. Bases de Datos Relacionales y Tablas
Una Base de Datos Relacional organiza la información en Tablas (muy similares a hojas de cálculo de Excel bien estructuradas):
- Columnas (Campos): Definen qué tipo de dato guardamos en cada celda (
TEXT,INTEGER,BOOLEAN). - Filas (Registros / Tuplas): Cada una representa un elemento individual (un enlace acortado, un usuario, un producto).
- Llave Primaria (
PRIMARY KEY): Una columna especial que garantiza que cada fila tenga un identificador único e irrepetible (id).
Tabla: "enlaces"┌─────┬─────────────────────────────────┬──────────────┬───────┬──────────────────────────┐│ id │ url_original │ codigo_corto │ clics │ fecha_creacion │├─────┼─────────────────────────────────┼──────────────┼───────┼──────────────────────────┤│ 1 │ https://developer.mozilla.org │ mdn-docs │ 15 │ 2026-08-18T10:00:00.000Z ││ 2 │ https://astro.build │ astro-web │ 42 │ 2026-08-18T11:30:00.000Z ││ 3 │ https://wikipedia.org │ wiki-main │ 8 │ 2026-08-18T12:15:00.000Z │└─────┴─────────────────────────────────┴──────────────┴───────┴──────────────────────────┘2. El Lenguaje SQL: Las 4 Operaciones Maestras (CRUD)
SQL (Structured Query Language) es el lenguaje estándar universal utilizado para dialogar con bases de datos. No importa si usas SQLite, PostgreSQL, MySQL o Oracle; los comandos esenciales son los mismos:
1. CREATE TABLE (Crear estructura)
CREATE TABLE IF NOT EXISTS enlaces ( id INTEGER PRIMARY KEY AUTOINCREMENT, url_original TEXT NOT NULL, codigo_corto TEXT UNIQUE NOT NULL, clics INTEGER DEFAULT 0, fecha_creacion TEXT);2. INSERT (Crear / Guardar un nuevo registro)
INSERT INTO enlaces (url_original, codigo_corto, fecha_creacion)VALUES ('https://github.com', 'gh-main', '2026-08-18');3. SELECT (Consultar / Buscar registros)
-- Buscar un enlace específico por su código cortoSELECT * FROM enlaces WHERE codigo_corto = 'gh-main';4. UPDATE (Modificar un registro existente)
-- Sumar un clic al enlaceUPDATE enlaces SET clics = clics + 1 WHERE codigo_corto = 'gh-main';3. ¿Por qué SQLite?
A diferencia de otros motores de bases de datos que requieren instalar servidores pesados y configurar contraseñas complejas, SQLite es una base de datos embebida en un solo archivo:
- Toda tu base de datos vive dentro de un archivo local en tu proyecto (
shorturl.db). - Es la base de datos más utilizada en el planeta: está dentro de todos los teléfonos iPhone y Android, dentro de navegadores web como Google Chrome y en aplicaciones como Spotify y WhatsApp.
- Cero configuración, ultra veloz y perfecta para nuestro proyecto.
🔍 Experimento en Vivo: Inspeccionando una Base de Datos SQLite
Vamos a ver cómo se ve una base de datos relacional:
- Crearemos una carpeta llamada
data/en nuestro proyecto. - Usaremos la librería
better-sqlite3en Node.js para crear el archivodata/shorturl.db. - Verás cómo SQLite genera un archivo binario real en tu disco duro que preservará todos los datos incluso si reinicias tu equipo.
💻 Fase 3: Práctica Asistida en Vivo
Integraremos SQLite en nuestro servidor ShortURL paso a paso.
🧩 Reto 1: Inicializar la Base de Datos SQLite (database.js)
Objetivo: Crear el módulo de conexión y asegurar que la tabla enlaces se cree automáticamente si no existe.
Crea un archivo llamado database.js en la raíz de tu proyecto:
// 1. Importamos better-sqlite3 y el módulo de rutas del sistemaconst Database = require("better-sqlite3");const path = require("path");const fs = require("fs");
// 2. Nos aseguramos de que exista la carpeta data/const carpetaData = path.join(__dirname, "data");if (!fs.existsSync(carpetaData)) { fs.mkdirSync(carpetaData);}
// 3. Abrimos o creamos el archivo de base de datosconst rutaDB = path.join(carpetaData, "shorturl.db");const db = new Database(rutaDB);
// 4. Creamos la tabla 'enlaces' con SQL puro si aún no existedb.exec(` CREATE TABLE IF NOT EXISTS enlaces ( id INTEGER PRIMARY KEY AUTOINCREMENT, url_original TEXT NOT NULL, codigo_corto TEXT UNIQUE NOT NULL, clics INTEGER DEFAULT 0, fecha_creacion TEXT NOT NULL );`);
console.log("📦 Base de datos SQLite conectada e inicializada con éxito.");
// Exportamos la instancia para usarla en server.jsmodule.exports = db;🧩 Reto 2: Guardar Enlaces con Consultas Preparadas (INSERT)
Objetivo: Reemplazar el arreglo en memoria en server.js por una sentencia SQL segura contra inyecciones.
Abre server.js e importa la base de datos:
const express = require("express");const db = require("./database"); // Importamos nuestra base de datos SQLiteconst app = express();const PUERTO = 3000;
app.use(express.json());app.use(express.static("public"));
function generarCodigo(longitud = 6) { const caracteres = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; let codigo = ""; for (let i = 0; i < longitud; i++) { codigo += caracteres[Math.floor(Math.random() * caracteres.length)]; } return codigo;}
// Endpoint POST: Guardar permanentemente en SQLiteapp.post("/api/acortar", (req, res) => { const { urlOriginal } = req.body;
if (!urlOriginal || !urlOriginal.startsWith("http")) { return res.status(400).json({ error: "La URL debe comenzar con http:// o https://" }); }
const codigo = generarCodigo(); const fecha = new Date().toISOString();
try { // 1. Preparamos la consulta SQL con parámetros seguros (?) const consulta = db.prepare(` INSERT INTO enlaces (url_original, codigo_corto, fecha_creacion) VALUES (?, ?, ?) `);
// 2. Ejecutamos la inserción con los valores reales consulta.run(urlOriginal, codigo, fecha);
console.log(`[SQL INSERT] Enlace guardado en disco: ${codigo} -> ${urlOriginal}`);
res.status(201).json({ exito: true, codigo: codigo, urlCorta: `http://localhost:${PUERTO}/${codigo}`, urlOriginal: urlOriginal });
} catch (error) { console.error("Error al guardar en base de datos:", error); res.status(500).json({ error: "Error interno al procesar el enlace." }); }});🧩 Reto 3: Buscar y Actualizar Clics con SQL (SELECT y UPDATE)
Objetivo: Modificar el endpoint GET /:codigo para consultar SQLite y aumentar el contador de visitas en el disco.
Agrega la lógica de búsqueda y actualización a server.js:
// Endpoint GET: Buscar código, actualizar clics y redirigirapp.get("/:codigo", (req, res) => { const codigoBuscado = req.params.codigo;
try { // 1. Buscamos el registro en SQLite usando SELECT y get() const consultaBusqueda = db.prepare("SELECT * FROM enlaces WHERE codigo_corto = ?"); const enlace = consultaBusqueda.get(codigoBuscado);
// Si no existe en la base de datos, enviamos 404 if (!enlace) { return res.status(404).send(` <h1 style="font-family: sans-serif; text-align: center; margin-top: 50px;"> ❌ Error 404: Enlace no encontrado en la base de datos </h1> <p style="text-align: center;"><a href="/">Volver a ShortURL</a></p> `); }
// 2. Incrementamos el contador en la base de datos con UPDATE const consultaActualizar = db.prepare(` UPDATE enlaces SET clics = clics + 1 WHERE codigo_corto = ? `); consultaActualizar.run(codigoBuscado);
console.log(`[SQL UPDATE] Redirigiendo ${codigoBuscado} a ${enlace.url_original} (Clics: ${enlace.clics + 1})`);
// 3. Redirigimos al usuario a su destino res.redirect(enlace.url_original);
} catch (error) { console.error("Error al consultar la base de datos:", error); res.status(500).send("Error del servidor"); }});🧩 Reto 4: Endpoint de Métricas Persistente (GET /api/metricas/:codigo)
Objetivo: Consultar la base de datos y devolver la información histórica de cualquier enlace.
app.get("/api/metricas/:codigo", (req, res) => { const codigoBuscado = req.params.codigo;
try { const consulta = db.prepare("SELECT * FROM enlaces WHERE codigo_corto = ?"); const enlace = consulta.get(codigoBuscado);
if (!enlace) { return res.status(404).json({ error: "El enlace no existe en la base de datos." }); }
res.json({ codigo: enlace.codigo_corto, urlOriginal: enlace.url_original, clicsTotales: enlace.clics, fechaCreacion: enlace.fecha_creacion });
} catch (error) { res.status(500).json({ error: "Error al consultar métricas." }); }});🗣️ Fase 4: Exposición de Resultados, Debate y Debugging
Probamos la persistencia en vivo: creamos enlaces, apagamos el servidor con Ctrl + C, lo encendemos de nuevo y comprobamos que los enlaces siguen funcionando.
🐞 Las 4 Trampas Clásicas de SQL y Bases de Datos
1. Inyección SQL (SQL Injection): El Error Más Peligroso
- El error fatal: Escribir consultas concatenando texto:
// ❌ ¡PELIGROSO! Vulnerable a ataques de inyección SQLdb.prepare("SELECT * FROM enlaces WHERE codigo = '" + inputUsuario + "'");
- La solución profesional: Usar Consultas Preparadas (Prepared Statements) con el signo
?:// ✅ 100% Seguro: SQLite trata la entrada como texto plano, nunca como código ejecutabledb.prepare("SELECT * FROM enlaces WHERE codigo = ?").get(inputUsuario);
2. Olvidar la cláusula WHERE en UPDATE o DELETE
- La pesadilla: Ejecutar
UPDATE enlaces SET clics = 0;sinWHERE. - Consecuencia: Modificará o borrará todas las filas de la tabla sin confirmación. Siempre verifica dos veces el
WHERE.
3. Confundir .run(), .get() y .all() en better-sqlite3
.run(params): Para operaciones que modifican datos (INSERT,UPDATE,DELETE)..get(params): Para consultasSELECTque devuelven una sola fila (oundefined)..all(params): Para consultasSELECTque devuelven un arreglo con múltiples filas.
4. Bloqueo de archivo de base de datos
- El síntoma:
SqliteError: database is locked. - La causa: Dos procesos de Node.js intentando escribir simultáneamente en el mismo archivo sin cerrar conexiones.
🏠 Fase 5: Definición de Práctica en Casa / Reto Semanal
Crearás un panel de enlaces populares para mostrar en la página principal.
🏆 Proyecto Semanal: “El Top 5 de Enlaces Más Populares”
Descripción del Problema: Queremos mostrar en nuestra interfaz cuáles son los 5 enlaces más visitados de toda la plataforma.
Especificaciones Técnicas a Implementar:
-
Nuevo Endpoint
GET /api/popularesenserver.js:- Escribe una consulta SQL que ordene los registros de forma descendente por clics y limite el resultado a 5 filas:
SELECT codigo_corto, url_original, clics FROM enlacesORDER BY clics DESCLIMIT 5;
- Ejecuta la consulta usando
db.prepare(...).all()y devuelve el arreglo en formato JSON.
- Escribe una consulta SQL que ordene los registros de forma descendente por clics y limite el resultado a 5 filas:
-
Mostrar la lista en el Frontend (
index.htmlyapp.js):- Agrega una sección
<section id="seccion-populares">debajo del formulario. - Crea una función
cargarEnlacesPopulares()enapp.jsque haga unfetch('/api/populares')y pinte una pequeña lista con los enlaces top y su número de visitas. - Llama a esta función al cargar la página y cada vez que el usuario cree un enlace nuevo.
- Agrega una sección
-
Prueba de Fuego de Persistencia:
- Crea 3 enlaces distintos y haz clics en ellos.
- Cierra por completo tu terminal y apaga tu computadora si lo deseas.
- Vuelve a abrir la terminal, ejecuta
node server.js, abrehttp://localhost:3000y verifica con orgullo que tus enlaces y su ranking popular siguen ahí intactos.
📚 Recursos y Videos Recomendados
Para profundizar en bases de datos relacionales, SQL y SQLite:
🚀 Conclusión y Próximos Pasos
¡Felicidades por completar el Sprint 2: Construcción Full-Stack de ShortURL!
En este momento has construido una aplicación web profesional de tres capas (Frontend + Backend + Base de Datos) que funciona de punta a punta.
Ahora que dominas el código de la aplicación, entraremos en la recta final del curso: el Bloque 4, donde aprenderemos las herramientas que utilizan los desarrolladores profesionales para trabajar a diario: En la próxima sesión iniciaremos con: Lección 9: Uso de la Terminal - Dominio de la Línea de Comandos CLI (L9).
🚀 Clases en Vivo — Inicio: Domingo 6 de Septiembre ($200 MXN / clase). Aprende a programar desde cero con acompañamiento directo y construye el proyecto ShortURL.