Conceptos esenciales para conectar una base de datos
Persistencia: por qué una base de datos
En una API, la persistencia es la capacidad de guardar información de forma duradera para que sobreviva a reinicios del servidor, despliegues o fallos. Sin persistencia, los datos quedarían en memoria (RAM) y se perderían al reiniciar el proceso de Node.js.
Una base de datos aporta: almacenamiento duradero, consultas eficientes, índices, control de concurrencia y mecanismos de integridad. En este capítulo usaremos MongoDB, una base de datos NoSQL orientada a documentos (JSON/BSON), muy común en proyectos Node.js.
Conexiones y pooling
Una API no debería abrir y cerrar una conexión a la base de datos en cada request. En su lugar, se mantiene una conexión (o un conjunto) reutilizable. En MongoDB, el driver y Mongoose gestionan internamente un pool de conexiones (un grupo de conexiones TCP) para atender múltiples operaciones concurrentes sin el coste de reconectar cada vez.
- Conexión única (gestionada por la librería): tu app llama a
connectuna vez al iniciar. - Pool: la librería mantiene varias conexiones para paralelismo y rendimiento.
- Reintentos y timeouts: se configuran para evitar que la app quede colgada ante problemas de red.
CRUD: Create, Read, Update, Delete
CRUD resume las operaciones básicas sobre datos:
- Create: insertar documentos.
- Read: consultar (por id, por filtros, paginación).
- Update: modificar campos.
- Delete: eliminar documentos.
En una API REST, normalmente se mapean a endpoints como POST /recurso, GET /recurso, PATCH /recurso/:id, DELETE /recurso/:id.
- Escuche el audio con la pantalla apagada.
- Obtenga un certificado al finalizar.
- ¡Más de 5000 cursos para que explores!
Descargar la aplicación
Modelos y esquemas (MongoDB + Mongoose)
MongoDB no obliga a un esquema rígido, pero en backend suele convenir validar y normalizar datos. Mongoose añade:
- Schema: define campos, tipos, validaciones, valores por defecto.
- Model: interfaz para hacer operaciones sobre una colección (crear, buscar, actualizar, borrar).
- Middleware/hooks: lógica antes/después de guardar, etc. (opcional).
Separación de acceso a datos (arquitectura limpia)
Para mantener el código mantenible, separa responsabilidades:
- Controllers: reciben request/response, validan lo mínimo, llaman a servicios.
- Services: contienen reglas de negocio y orquestan operaciones.
- Data layer (repositorios/DAO): encapsula consultas a la base de datos (Mongoose/driver). El resto de la app no debería depender de detalles de MongoDB.
Esta separación facilita testear, cambiar la base de datos en el futuro y evitar controladores “gordos”.
Guía práctica: MongoDB con Mongoose en una arquitectura controllers → services → data layer
1) Instalar Mongoose
En tu proyecto:
npm i mongoose2) Variables de entorno necesarias
Usaremos una variable para la URI de MongoDB (ejemplos típicos):
- Local:
mongodb://127.0.0.1:27017/miapp - Atlas (cloud):
mongodb+srv://usuario:pass@cluster0.xxxxx.mongodb.net/miapp
Asumiremos que ya tienes un mecanismo de variables de entorno en tu app. La clave que usaremos será MONGODB_URI.
3) Capa de infraestructura: conexión a MongoDB
Crea un módulo dedicado a la conexión, por ejemplo src/data/mongo/connection.js. La idea es conectar una sola vez al iniciar la app y exponer funciones para conectar/desconectar (útil en tests o shutdown).
const mongoose = require('mongoose'); let isConnected = false; async function connectMongo() { if (isConnected) return; const uri = process.env.MONGODB_URI; if (!uri) { throw new Error('MONGODB_URI no está definida'); } mongoose.connection.on('connected', () => { console.log('[mongo] connected'); }); mongoose.connection.on('error', (err) => { console.error('[mongo] connection error', err); }); mongoose.connection.on('disconnected', () => { console.log('[mongo] disconnected'); }); await mongoose.connect(uri, { autoIndex: true, serverSelectionTimeoutMS: 5000 }); isConnected = true; } async function disconnectMongo() { if (!isConnected) return; await mongoose.disconnect(); isConnected = false; } module.exports = { connectMongo, disconnectMongo };Notas prácticas:
serverSelectionTimeoutMSevita esperas largas si el servidor no responde.autoIndexes cómodo en desarrollo; en producción se suele gestionar con cuidado por rendimiento.- Los eventos (
connected,error,disconnected) ayudan a diagnosticar problemas.
4) Definir un esquema y modelo: ejemplo “Task”
Crearemos una entidad simple para practicar CRUD: tareas con título y estado. Archivo: src/data/mongo/models/task.model.js.
const mongoose = require('mongoose'); const TaskSchema = new mongoose.Schema( { title: { type: String, required: true, trim: true, minlength: 1, maxlength: 120 }, done: { type: Boolean, default: false } }, { timestamps: true } ); module.exports = mongoose.model('Task', TaskSchema);Qué aporta aquí el esquema: valida tipos, obliga title, recorta espacios, y añade createdAt/updatedAt automáticamente.
5) Data layer: repositorio para encapsular Mongoose
El repositorio expone funciones de acceso a datos sin que el resto de la app conozca Mongoose. Archivo: src/data/repositories/task.repository.js.
const Task = require('../mongo/models/task.model'); async function createTask({ title }) { const doc = await Task.create({ title }); return doc.toObject(); } async function listTasks() { const docs = await Task.find().sort({ createdAt: -1 }).lean(); return docs; } async function updateTask(id, patch) { const doc = await Task.findByIdAndUpdate( id, { $set: patch }, { new: true, runValidators: true } ).lean(); return doc; } async function deleteTask(id) { const doc = await Task.findByIdAndDelete(id).lean(); return doc; } async function getTaskById(id) { const doc = await Task.findById(id).lean(); return doc; } module.exports = { createTask, listTasks, updateTask, deleteTask, getTaskById };Detalles importantes:
lean()devuelve objetos planos (más rápido y simple para APIs).runValidators: trueaplica validaciones del schema en updates.- El repositorio no decide respuestas HTTP; solo devuelve datos o
null.
6) Service layer: reglas de negocio y validaciones de dominio
Los servicios llaman al repositorio y aplican reglas. Archivo: src/services/task.service.js.
const taskRepo = require('../data/repositories/task.repository'); function assertNonEmptyString(value, name) { if (typeof value !== 'string' || value.trim().length === 0) { const err = new Error(`${name} debe ser un string no vacío`); err.statusCode = 400; throw err; } } async function createTask(input) { assertNonEmptyString(input.title, 'title'); return taskRepo.createTask({ title: input.title.trim() }); } async function listTasks() { return taskRepo.listTasks(); } async function updateTask(id, patch) { if (patch.title !== undefined) assertNonEmptyString(patch.title, 'title'); if (patch.done !== undefined && typeof patch.done !== 'boolean') { const err = new Error('done debe ser boolean'); err.statusCode = 400; throw err; } const updated = await taskRepo.updateTask(id, patch); if (!updated) { const err = new Error('Task no encontrada'); err.statusCode = 404; throw err; } return updated; } async function deleteTask(id) { const deleted = await taskRepo.deleteTask(id); if (!deleted) { const err = new Error('Task no encontrada'); err.statusCode = 404; throw err; } return deleted; } module.exports = { createTask, listTasks, updateTask, deleteTask };Aquí se ve la ventaja de separar capas: el servicio define qué significa “válido” para tu dominio y cómo reaccionar si no existe un recurso.
7) Controllers: endpoints Express que llaman a servicios
Archivo: src/controllers/task.controller.js.
const taskService = require('../services/task.service'); async function createTask(req, res, next) { try { const task = await taskService.createTask(req.body); res.status(201).json({ data: task }); } catch (err) { next(err); } } async function listTasks(req, res, next) { try { const tasks = await taskService.listTasks(); res.json({ data: tasks }); } catch (err) { next(err); } } async function updateTask(req, res, next) { try { const task = await taskService.updateTask(req.params.id, req.body); res.json({ data: task }); } catch (err) { next(err); } } async function deleteTask(req, res, next) { try { const task = await taskService.deleteTask(req.params.id); res.json({ data: task }); } catch (err) { next(err); } } module.exports = { createTask, listTasks, updateTask, deleteTask };8) Rutas: mapear CRUD a endpoints
Archivo: src/routes/task.routes.js.
const express = require('express'); const controller = require('../controllers/task.controller'); const router = express.Router(); router.post('/tasks', controller.createTask); router.get('/tasks', controller.listTasks); router.patch('/tasks/:id', controller.updateTask); router.delete('/tasks/:id', controller.deleteTask); module.exports = router;9) Conectar a MongoDB al iniciar la app
En tu punto de arranque (por ejemplo src/app.js o src/server.js), conecta antes de escuchar el puerto. La idea: si la base de datos no está disponible, prefieres fallar rápido en el arranque (dependiendo del caso de uso).
const express = require('express'); const { connectMongo } = require('./data/mongo/connection'); const taskRoutes = require('./routes/task.routes'); const app = express(); app.use(express.json()); app.use('/api', taskRoutes); async function start() { await connectMongo(); const port = process.env.PORT || 3000; app.listen(port, () => console.log(`API escuchando en ${port}`)); } start().catch((err) => { console.error('Error al iniciar', err); process.exit(1); });10) Probar el CRUD (ejemplos de requests)
Crear una tarea:
POST /api/tasks Content-Type: application/json { "title": "Aprender MongoDB con Mongoose" }Listar tareas:
GET /api/tasksActualizar una tarea (marcar como hecha):
PATCH /api/tasks/65c1f2... Content-Type: application/json { "done": true }Eliminar una tarea:
DELETE /api/tasks/65c1f2...Buenas prácticas específicas al trabajar con MongoDB/Mongoose
Evitar lógica de negocio en el modelo
Mongoose permite métodos y middleware en el schema, pero en proyectos pequeños/medianos suele ser más claro mantener reglas de negocio en services y dejar el modelo como definición de datos y validación básica.
Validar IDs y errores comunes
Cuando un :id no tiene formato válido, Mongoose puede lanzar un error de casteo (CastError). Puedes manejarlo en tu middleware de errores y devolver 400. Si el id es válido pero no existe, el repositorio devuelve null y el servicio puede responder 404 (como hicimos).
Índices y rendimiento
Si vas a filtrar por campos frecuentemente (por ejemplo, done), considera índices en el schema:
TaskSchema.index({ done: 1, createdAt: -1 });En producción, planifica la creación de índices para evitar impactos al arrancar.
Separación por capas: checklist rápido
| Capa | Responsabilidad | Ejemplo |
|---|---|---|
| Controller | HTTP: params/body, status codes, delegar | res.status(201).json(...) |
| Service | Reglas de negocio, validación de dominio | si no existe → 404 |
| Repository/Data | Consultas a MongoDB | findByIdAndUpdate |
| Connection | Conectar, eventos, configuración | mongoose.connect |