Conexión a base de datos en Node.js: conceptos y ejemplo con MongoDB

Capítulo 10

Tiempo estimado de lectura: 8 minutos

+ Ejercicio

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 connect una 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.

Continúa en nuestra aplicación.
  • Escuche el audio con la pantalla apagada.
  • Obtenga un certificado al finalizar.
  • ¡Más de 5000 cursos para que explores!
O continúa leyendo más abajo...
Download App

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 mongoose

2) 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:

  • serverSelectionTimeoutMS evita esperas largas si el servidor no responde.
  • autoIndex es 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: true aplica 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/tasks

Actualizar 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

CapaResponsabilidadEjemplo
ControllerHTTP: params/body, status codes, delegarres.status(201).json(...)
ServiceReglas de negocio, validación de dominiosi no existe → 404
Repository/DataConsultas a MongoDBfindByIdAndUpdate
ConnectionConectar, eventos, configuraciónmongoose.connect

Ahora responde el ejercicio sobre el contenido:

¿Cuál es la razón principal para conectar a MongoDB una sola vez al iniciar la app (en lugar de abrir y cerrar una conexión por cada request)?

¡Tienes razón! Felicitaciones, ahora pasa a la página siguiente.

¡Tú error! Inténtalo de nuevo.

En una API se recomienda conectar una vez al arrancar para reutilizar conexiones con un pool. Esto evita el costo de abrir/cerrar conexiones en cada request y permite atender operaciones concurrentes con mejor rendimiento.

Siguiente capítulo

Buenas prácticas de organización del código en backends Node.js

Arrow Right Icon
Portada de libro electrónico gratuitaNode.js para principiantes: crea un backend simple con Express
83%

Node.js para principiantes: crea un backend simple con Express

Nuevo curso

12 páginas

Descarga la aplicación para obtener una certificación gratuita y escuchar cursos en segundo plano, incluso con la pantalla apagada.