← Blog

API REST con Express y MongoDB: arquitectura que escala desde el primer día

D
Damián Oliva
·18 de marzo de 2025

El problema de la arquitectura prematura (y de la ausente)

Hay dos errores comunes al empezar un proyecto Node.js: sobre-arquitecturar desde el día uno con microservicios y capas de abstracción innecesarias, o poner todo en un archivo index.js de 800 líneas. Ninguno escala bien.

La estructura que voy a describir es la que uso en la mayoría de mis proyectos: lo suficientemente organizada para crecer, lo suficientemente simple para moverse rápido.

Estructura de carpetas

backend/
├── config/
│   └── db.js
├── middleware/
│   ├── auth.js
│   ├── errorHandler.js
│   └── validate.js
├── models/
│   └── User.js
├── routes/
│   └── users.js
├── controllers/
│   └── userController.js
├── services/
│   └── emailService.js
└── index.js

La diferencia entre routes, controllers y services

  • Routes: solo definen el endpoint y apuntan al controller. Sin lógica.
  • Controllers: reciben el request, llaman al servicio o al modelo, devuelven la response. Lógica HTTP solamente.
  • Services: lógica de negocio pura. No saben nada de HTTP. Fáciles de testear.
// routes/users.js
import { Router } from 'express';
import { getUsers, createUser } from '../controllers/userController.js';
import { auth } from '../middleware/auth.js';

const router = Router();
router.get('/', auth, getUsers);
router.post('/', createUser);
export default router;

// controllers/userController.js
import { findAllUsers, createNewUser } from '../services/userService.js';

export const getUsers = async (req, res, next) => {
  try {
    const users = await findAllUsers(req.query);
    res.json(users);
  } catch (err) { next(err); }
};

Manejo global de errores

// middleware/errorHandler.js
export const errorHandler = (err, req, res, next) => {
  const status = err.status || 500;
  const message = err.message || 'Error interno del servidor';

  if (process.env.NODE_ENV !== 'production') {
    console.error(err.stack);
  }

  res.status(status).json({
    error: message,
    ...(process.env.NODE_ENV !== 'production' && { stack: err.stack }),
  });
};

// En index.js, siempre al final:
app.use(errorHandler);

Validación de requests

import Joi from 'joi';

export const validate = (schema) => (req, res, next) => {
  const { error } = schema.validate(req.body, { abortEarly: false });
  if (error) {
    return res.status(400).json({
      error: 'Validación fallida',
      details: error.details.map(d => d.message),
    });
  }
  next();
};

// Uso:
const createUserSchema = Joi.object({
  name: Joi.string().min(2).required(),
  email: Joi.string().email().required(),
  password: Joi.string().min(8).required(),
});

router.post('/', validate(createUserSchema), createUser);

Paginación estándar

export const findAllUsers = async ({ page = 1, limit = 20, search }) => {
  const query = search ? { name: new RegExp(search, 'i') } : {};
  const skip = (page - 1) * limit;

  const [items, total] = await Promise.all([
    User.find(query).skip(skip).limit(Number(limit)).lean(),
    User.countDocuments(query),
  ]);

  return {
    items,
    pagination: { page: Number(page), limit: Number(limit), total, pages: Math.ceil(total / limit) },
  };
};
Newsletter

Ideas directamente
a tu bandeja de entrada

Comparto lo que estoy construyendo, lo que aprendo y lo que me parece interesante del mundo tech y startups. Sin spam. Cuando tenga algo que valga la pena.

Sin spam. Unsubscribe en cualquier momento.