Arquitectura en Capas con Express, Sequelize y PostgreSQL

Jorge SaavedraJorge Saavedra
·18 de enero, 2026·9 min de lectura
node-jsexpresssequelizepostgresqlarquitecturaapi-restdockerorm

Repositorio del Proyecto

El código completo de este tutorial está disponible en: GitHub Repository

Una nota antes de empezar

Seré honesto contigo. No soy el mayor fanático de la arquitectura en capas. En proyectos reales, siempre opto por la arquitectura hexagonal (o puertos y adaptadores), que ofrece mayor flexibilidad y testabilidad.
Sin embargo, este tutorial está diseñado específicamente para mis estudiantes que están dando sus primeros pasos en el mundo del desarrollo web backend. La arquitectura en capas es más fácil de entender cuando se está comenzando, y prepara el camino hacia conceptos más avanzados. Piensa en esto como aprender a caminar antes de correr. Una vez que domines estos fundamentos, estarás listo para explorar patrones más sofisticados.

Qué vamos a construir?

En este tutorial completo, crearemos un sistema de facturación con Express.js, PostgreSQL y Sequelize. El proyecto está diseñado para estudiantes que están aprendiendo desarrollo web backend y quieren entender:
  • Arquitectura en capas (Controllers, Services, Repositories)
  • Diferencias entre SQL directo y ORM
    ORM

    Object-Relational Mapping, capa de abstracción que permite interactuar con la base de datos usando objetos de JavaScript en vez de SQL.

  • Relaciones entre tablas (1:N, N:M)
  • Docker para desarrollo local
  • Buenas prácticas de código limpio

Stack Tecnológico

  • Express.js: Framework minimalista de Node.js para construir APIs REST de forma rápida y flexible.
  • Sequelize ORM: Object-Relational Mapping para trabajar con bases de datos usando JavaScript. Simplifica las queries y maneja relaciones automáticamente.
  • pg (node-postgres): Driver nativo de PostgreSQL para escribir SQL directo. Te permite tener control total sobre tus queries.
  • PostgreSQL: Base de datos relacional open-source, robusta y confiable. Corriendo en Docker para facilitar el desarrollo local.
  • Docker: Contenerización para ejecutar PostgreSQL sin instalación compleja. Facilita el setup del ambiente de desarrollo.
  • Arquitectura en Capas: Separación de responsabilidades: Routes, Services, Repositories. Cada capa tiene un propósito específico.

Por qué estas tecnologías?

Este stack es muy común en la industria. PostgreSQL es robusto y gratuito, Express es el framework más popular de Node.js, y Sequelize facilita trabajar con bases de datos sin escribir SQL (aunque aprenderás ambas formas).

Qué es la Arquitectura en Capas?

La arquitectura en capas es un patrón de diseño que separa tu aplicación en capas independientes, cada una con una responsabilidad específica. Esto hace que tu código sea:
  • Mantenible: Cada capa tiene su propia responsabilidad. Los cambios en una capa no afectan a las demás.
  • Testeable: Puedes probar cada capa por separado sin necesidad de la base de datos real.
  • Escalable: Fácil agregar nuevas funcionalidades sin romper el código existente.

Las 3 Capas de Nuestra Arquitectura

1. Controllers (Routes)

Responsabilidad: Manejar las peticiones HTTP (GET, POST, PUT, DELETE)
  • Recibe la petición del cliente
  • Valida parámetros básicos
  • Llama al Service correspondiente
  • Devuelve la respuesta (JSON)

2. Services

Responsabilidad: Lógica de negocio y reglas de validación
  • Valida datos de negocio
  • Orquesta operaciones complejas
  • Llama a uno o más Repositories
  • Lanza excepciones si algo falla

3. Repositories

Responsabilidad: Acceso directo a la base de datos
  • Operaciones CRUD (Create, Read, Update, Delete)
  • Queries a la base de datos
  • NO tiene lógica de negocio
  • Independiente del ORM o SQL usado

Configuración Inicial del Proyecto

Prerequisitos

Antes de empezar, asegúrate de tener instalado:
  • Node.js (versión 16 o superior)
  • Docker y Docker Compose
  • Un editor de código (VS Code recomendado)
  • Postman o Thunder Client para probar la API

1. Estructura del Proyecto

bash
mkdir express-architecture
cd express-architecture
npm init -y

2. Instalar Dependencias

bash
npm install express pg sequelize
Donde:
  • express - Framework web para Node.js
  • pg - Driver de PostgreSQL para SQL directo
  • sequelize - ORM para manejar la base de datos con JavaScript

3. Configurar Docker Compose

Crea un archivo docker-compose.yml en la raíz del proyecto:
docker-compose.yml
version: '3.8'

services:
  postgres:
    image: postgres:16-alpine
    container_name: express_002_postgres
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: products_db
    ports:
      - "5433:5432"  # Puerto personalizado para evitar conflictos
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql

volumes:
  postgres_data:

Por qué puerto 5433?

Usamos el puerto 5433 en lugar del 5432 por defecto para evitar conflictos si ya tienes PostgreSQL instalado en tu máquina.

4. Script Inicial de la Base de Datos

Crea init.sql con las tablas iniciales:
init.sql
-- Tabla de Usuarios
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(100) UNIQUE NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Tabla de Productos
CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    description VARCHAR(200) NOT NULL,
    price DECIMAL(10,2) NOT NULL,
    stock INTEGER DEFAULT 0,
    sku VARCHAR(50) UNIQUE NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Tabla de Facturas
CREATE TABLE invoices (
    id SERIAL PRIMARY KEY,
    user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
    total DECIMAL(10,2) NOT NULL,
    invoice_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Tabla debil: Items de Factura (relacion N:M)
CREATE TABLE invoice_items (
    id SERIAL PRIMARY KEY,
    invoice_id INTEGER REFERENCES invoices(id) ON DELETE CASCADE,
    product_id INTEGER REFERENCES products(id) ON DELETE CASCADE,
    quantity INTEGER NOT NULL,
    unit_price DECIMAL(10,2) NOT NULL,
    subtotal DECIMAL(10,2) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Datos de ejemplo
INSERT INTO users (name, email) VALUES
('Jorge Saavedra', 'jorge@example.com'),
('Ana Garcia', 'ana@example.com');

INSERT INTO products (description, price, stock, sku) VALUES
('Laptop Dell XPS', 1299.99, 10, 'LAP-DELL-001'),
('Mouse Logitech', 29.99, 50, 'MOU-LOG-002'),
('Teclado Mecanico', 89.99, 25, 'KEY-MEC-003');

5. Iniciar PostgreSQL

bash
# Iniciar PostgreSQL
docker-compose up -d

# Verificar que este corriendo
docker ps

# Conectarse a la base de datos (opcional)
docker exec -it express_002_postgres psql -U postgres -d products_db

SQL Directo vs ORM: Dos Formas de Trabajar

Una de las características únicas de este proyecto es que implementaremos la misma funcionalidad de dos formas diferentes: usando SQL directo (con el driver pg) y usando un ORM (Sequelize).

Por qué aprender ambas?

Como estudiante, es crucial que entiendas cómo funcionan las bases de datos por dentro(SQL directo) antes de usar abstracciones (ORM). Una vez que domines SQL, los ORMs te harán mucho más productivo.

Comparación entre SQL Directo y ORM

CaracterísticaSQL Directo (pg)ORM (Sequelize)
ControlTotal control sobre queriesLa abstracción puede limitar
Curva de aprendizajeNecesitas saber SQLMás fácil de empezar
ProductividadMás código manualMenos código, más rápido
DebuggingVes exactamente el SQLQueries generadas automáticamente
MigracionesManualAutomáticas
RelacionesJOINs manualesIncludes automáticos
Casos de usoQueries complejas, optimizaciónCRUD rápido, prototipado

Ejemplo Comparativo: Obtener Todos los Productos

Con SQL directo (pg)

javascript
// productRepository.js
class ProductRepository {
    constructor(pool) {
        this.pool = pool;
    }

    async findAll() {
        const result = await this.pool.query(
            'SELECT * FROM products ORDER BY id'
        );
        return result.rows;
    }

    async findById(id) {
        const result = await this.pool.query(
            'SELECT * FROM products WHERE id = $1',
            [id]
        );
        return result.rows[0];
    }
}

Con Sequelize (ORM)

javascript
// productRepositoryORM.js
class ProductRepositoryORM {
    constructor(model) {
        this.model = model;
    }

    async findAll() {
        return await this.model.findAll({
            order: [['id', 'ASC']]
        });
    }

    async findById(id) {
        return await this.model.findByPk(id);
    }
}
Como puedes ver, con SQL directo escribes la consulta completa, mientras que con Sequelize usas métodos de JavaScript que generan el SQL por ti.

Definiendo Modelos con Sequelize

Los modelos son clases de JavaScript que representan tablas de la base de datos. Sequelize nos permite definir la estructura de nuestras tablas usando código. Ver documentación de Modelos

1. Configuración de Sequelize

database.js
const { Sequelize } = require('sequelize');

const sequelize = new Sequelize('products_db', 'postgres', 'postgres', {
    host: 'localhost',
    port: 5433,
    dialect: 'postgres',
    logging: false, // Desactiva los logs SQL en consola
    pool: {
        max: 5,
        min: 0,
        acquire: 30000,
        idle: 10000
    }
});

// Probar la conexion
async function testConnection() {
    try {
        await sequelize.authenticate();
        console.log('[OK] Conexion a PostgreSQL exitosa');
    } catch (error) {
        console.error('[ERROR] Error conectando a PostgreSQL:', error);
    }
}

testConnection();

module.exports = sequelize;

2. Modelo de Producto

models/Product.js
const { DataTypes } = require('sequelize');
const sequelize = require('../database');

const Product = sequelize.define('Product', {
    id: {
        type: DataTypes.INTEGER,
        primaryKey: true,
        autoIncrement: true
    },
    description: {
        type: DataTypes.STRING(200),
        allowNull: false
    },
    price: {
        type: DataTypes.DECIMAL(10, 2),
        allowNull: false
    },
    stock: {
        type: DataTypes.INTEGER,
        defaultValue: 0
    },
    sku: {
        type: DataTypes.STRING(50),
        allowNull: false,
        unique: true
    }
}, {
    tableName: 'products',
    timestamps: true,
    createdAt: 'created_at',
    updatedAt: false
});

module.exports = Product;

Sincronización de Modelos

En este proyecto, NO usamos sequelize.sync() porque ya tenemos las tablas creadas con init.sql. Sequelize solo se conecta a tablas existentes. En proyectos nuevos, podrías usar sync() o migraciones.

Relaciones entre Tablas: El Corazón del Sistema

Nuestro sistema de facturación tiene cuatro tablas relacionadas. Entender estas relaciones es fundamental para trabajar con bases de datos relacionales. Ver documentación de Asociaciones

Diagrama de Relaciones

El diagrama de entidad-relación del sistema incluye las siguientes tablas y relaciones:
  • users: id (PK), name, email (UK), created_at
  • products: id (PK), description, price, stock, sku (UK), created_at
  • invoices: id (PK), user_id (FK), total, invoice_date, created_at
  • invoice_items: id (PK), invoice_id (FK), product_id (FK), quantity, unit_price, subtotal, created_at
Leyenda: PK = Primary Key (Clave Primaria), FK = Foreign Key (Clave Foránea), UK = Unique Key (Único). Las relaciones son de tipo Uno a Muchos (1:N).

Tipos de Relaciones

1:N - Un Usuario tiene Muchas Facturas
Un usuario puede tener múltiples facturas, pero cada factura pertenece a un solo usuario.
javascript
// En associations.js
User.hasMany(Invoice, {
    foreignKey: 'user_id',
    as: 'invoices'
});

Invoice.belongsTo(User, {
    foreignKey: 'user_id',
    as: 'user'
});
N:M - Facturas y Productos (a través de invoice_items)
Una factura puede tener muchos productos, y un producto puede estar en muchas facturas. Esta relación se implementa con una tabla débil(invoice_items).
javascript
// En associations.js
Invoice.belongsToMany(Product, {
    through: InvoiceItem,
    foreignKey: 'invoice_id',
    otherKey: 'product_id',
    as: 'products'
});

Product.belongsToMany(Invoice, {
    through: InvoiceItem,
    foreignKey: 'product_id',
    otherKey: 'invoice_id',
    as: 'invoices'
});

Qué es una Tabla Débil?

invoice_items es una tabla débil porque:
  • No tiene sentido por sí sola (necesita una factura Y un producto)
  • Depende de otras tablas para existir
  • Si eliminas una factura, sus items también se eliminan (ON DELETE CASCADE)
  • Almacena información adicional de la relación (cantidad, precio en ese momento, subtotal)

Consultando con Relaciones (include)

Una de las ventajas de Sequelize es que puedes cargar relaciones automáticamentesin escribir JOINs manuales. Aprende más sobre Eager Loading
Ejemplo: Obtener factura con usuario y productos
// Obtener una factura con todos sus datos relacionados
const invoice = await Invoice.findByPk(1, {
    include: [
        {
            model: User,
            as: 'user',
            attributes: ['id', 'name', 'email']
        },
        {
            model: InvoiceItem,
            as: 'items',
            include: [{
                model: Product,
                as: 'product'
            }]
        }
    ]
});

// Resultado:
{
    id: 1,
    total: 1419.97,
    invoice_date: '2025-01-31T...',
    user: {
        id: 1,
        name: 'Jorge Saavedra',
        email: 'jorge@example.com'
    },
    items: [
        {
            quantity: 1,
            unit_price: 1299.99,
            subtotal: 1299.99,
            product: {
                description: 'Laptop Dell XPS',
                price: 1299.99,
                sku: 'LAP-DELL-001'
            }
        },
        {
            quantity: 4,
            unit_price: 29.99,
            subtotal: 119.98,
            product: {
                description: 'Mouse Logitech',
                price: 29.99,
                sku: 'MOU-LOG-002'
            }
        }
    ]
}

Implementando Repositorios

Los repositorios son la capa que accede directamente a la base de datos. Implementaremos dos versiones del repositorio de productos para que entiendas ambas formas.

Repositorio con SQL Directo

repositories/productRepository.js
const { Pool } = require('pg');

class ProductRepository {
    constructor() {
        this.pool = new Pool({
            user: 'postgres',
            host: 'localhost',
            database: 'products_db',
            password: 'postgres',
            port: 5433,
        });
    }

    async findAll() {
        const result = await this.pool.query(
            'SELECT * FROM products ORDER BY id'
        );
        return result.rows;
    }

    async findById(id) {
        const result = await this.pool.query(
            'SELECT * FROM products WHERE id = $1',
            [id]
        );
        return result.rows[0];
    }

    async create(productData) {
        const { description, price, stock, sku } = productData;
        const result = await this.pool.query(
            `INSERT INTO products (description, price, stock, sku)
             VALUES ($1, $2, $3, $4)
             RETURNING *`,
            [description, price, stock, sku]
        );
        return result.rows[0];
    }

    async update(id, productData) {
        const { description, price, stock, sku } = productData;
        const result = await this.pool.query(
            `UPDATE products
             SET description = $1, price = $2, stock = $3, sku = $4
             WHERE id = $5
             RETURNING *`,
            [description, price, stock, sku, id]
        );
        return result.rows[0];
    }

    async delete(id) {
        await this.pool.query('DELETE FROM products WHERE id = $1', [id]);
        return true;
    }
}

module.exports = new ProductRepository();

Repositorio con Sequelize (ORM)

repositories/productRepositoryORM.js
const Product = require('../models/Product');

class ProductRepositoryORM {
    async findAll() {
        return await Product.findAll({
            order: [['id', 'ASC']]
        });
    }

    async findById(id) {
        return await Product.findByPk(id);
    }

    async create(productData) {
        return await Product.create(productData);
    }

    async update(id, productData) {
        const product = await Product.findByPk(id);
        if (!product) return null;

        return await product.update(productData);
    }

    async delete(id) {
        const product = await Product.findByPk(id);
        if (!product) return false;

        await product.destroy();
        return true;
    }
}

module.exports = new ProductRepositoryORM();

Firma de Métodos Idéntica

Nota cómo ambos repositorios tienen exactamente los mismos métodos con los mismos parámetros. Esto te permite cambiar entre SQL directo y ORM sin modificar el resto del código.

Capa de Servicios: La Lógica de Negocio

Los servicios contienen la lógica de negocio. Aquí es donde validamos datos, manejamos errores y orquestamos operaciones que pueden involucrar múltiples repositorios.
services/productService.js
// Puedes cambiar entre ambos repositorios aqui:
const productRepository = require('../repositories/productRepositoryORM');
// const productRepository = require('../repositories/productRepository');

class ProductService {
    async getAllProducts() {
        try {
            return await productRepository.findAll();
        } catch (error) {
            throw new Error('Error obteniendo productos: ' + error.message);
        }
    }

    async getProductById(id) {
        // Validacion de negocio
        if (!id || isNaN(id)) {
            throw new Error('ID de producto invalido');
        }

        const product = await productRepository.findById(id);

        if (!product) {
            throw new Error(`Producto con ID ${id} no encontrado`);
        }

        return product;
    }

    async createProduct(productData) {
        // Validaciones de negocio
        if (!productData.description || !productData.price || !productData.sku) {
            throw new Error('Faltan campos obligatorios');
        }

        if (productData.price <= 0) {
            throw new Error('El precio debe ser mayor a 0');
        }

        if (productData.stock < 0) {
            throw new Error('El stock no puede ser negativo');
        }

        return await productRepository.create(productData);
    }

    async updateProduct(id, productData) {
        // Verificar que el producto existe
        await this.getProductById(id);

        // Validaciones de negocio
        if (productData.price && productData.price <= 0) {
            throw new Error('El precio debe ser mayor a 0');
        }

        if (productData.stock && productData.stock < 0) {
            throw new Error('El stock no puede ser negativo');
        }

        return await productRepository.update(id, productData);
    }

    async deleteProduct(id) {
        // Verificar que el producto existe
        await this.getProductById(id);

        return await productRepository.delete(id);
    }
}

module.exports = new ProductService();

Separación de Responsabilidades

Los servicios se encargan de la lógica de negocio (validar precio > 0, stock no negativo), mientras que los repositorios solo se encargan de guardar/recuperar datos.

Controllers (Routes): Manejando Peticiones HTTP

Los controllers (en Express llamados "routes") son la capa más externa. Reciben las peticiones HTTP del cliente, llaman al servicio correspondiente y devuelven la respuesta al cliente.
routes/productRoutes.js
const express = require('express');
const router = express.Router();
const productService = require('../services/productService');

// GET /api/products - Obtener todos los productos
router.get('/', async (req, res) => {
    try {
        const products = await productService.getAllProducts();
        res.json(products);
    } catch (error) {
        res.status(500).json({
            error: error.message
        });
    }
});

// GET /api/products/:id - Obtener un producto por ID
router.get('/:id', async (req, res) => {
    try {
        const product = await productService.getProductById(req.params.id);
        res.json(product);
    } catch (error) {
        res.status(404).json({
            error: error.message
        });
    }
});

// POST /api/products - Crear un nuevo producto
router.post('/', async (req, res) => {
    try {
        const newProduct = await productService.createProduct(req.body);
        res.status(201).json(newProduct);
    } catch (error) {
        res.status(400).json({
            error: error.message
        });
    }
});

// PUT /api/products/:id - Actualizar un producto
router.put('/:id', async (req, res) => {
    try {
        const updatedProduct = await productService.updateProduct(
            req.params.id,
            req.body
        );
        res.json(updatedProduct);
    } catch (error) {
        res.status(400).json({
            error: error.message
        });
    }
});

// DELETE /api/products/:id - Eliminar un producto
router.delete('/:id', async (req, res) => {
    try {
        await productService.deleteProduct(req.params.id);
        res.status(204).send();
    } catch (error) {
        res.status(404).json({
            error: error.message
        });
    }
});

module.exports = router;

Servidor Principal

server.js
const express = require('express');
const productRoutes = require('./routes/productRoutes');

const app = express();
const PORT = 4000;

// Middleware
app.use(express.json());

// Routes
app.use('/api/products', productRoutes);

// Servidor
app.listen(PORT, () => {
    console.log('Servidor corriendo en http://localhost:' + PORT);
    console.log('Endpoints disponibles:');
    console.log('   GET    /api/products');
    console.log('   GET    /api/products/:id');
    console.log('   POST   /api/products');
    console.log('   PUT    /api/products/:id');
    console.log('   DELETE /api/products/:id');
});

Probando la API

1. Iniciar el Servidor

bash
# Terminal 1: Asegurate de que PostgreSQL este corriendo
docker-compose up -d

# Terminal 2: Inicia el servidor Express
npm start

2. Probar con cURL

Obtener todos los productos:
bash
curl http://localhost:4000/api/products
Obtener un producto por ID:
bash
curl http://localhost:4000/api/products/1
Crear un nuevo producto:
bash
curl -X POST http://localhost:4000/api/products \
  -H "Content-Type: application/json" \
  -d '{
    "description": "Monitor LG 27 pulgadas",
    "price": 299.99,
    "stock": 15,
    "sku": "MON-LG-005"
  }'
Actualizar un producto:
bash
curl -X PUT http://localhost:4000/api/products/1 \
  -H "Content-Type: application/json" \
  -d '{
    "price": 1199.99,
    "stock": 8
  }'
Eliminar un producto:
bash
curl -X DELETE http://localhost:4000/api/products/1

Usando Postman o Thunder Client

Si prefieres una interfaz gráfica, puedes usar Postman o la extensión Thunder Client de VS Code para probar los endpoints de forma más visual.

Extendiendo el Sistema: Facturas e Items

Ahora que entiendes la arquitectura básica, podemos agregar más entidades. El proyecto incluye un sistema completo de facturación con users, invoices e invoice_items.

Repositorio de Facturas (con relaciones)

repositories/invoiceRepository.js
const { Invoice, User, InvoiceItem, Product } = require('../models');

class InvoiceRepository {
    // Obtener todas las facturas con usuario e items
    async findAll() {
        return await Invoice.findAll({
            include: [
                {
                    model: User,
                    as: 'user',
                    attributes: ['id', 'name', 'email']
                },
                {
                    model: InvoiceItem,
                    as: 'items',
                    include: [{
                        model: Product,
                        as: 'product'
                    }]
                }
            ],
            order: [['id', 'DESC']]
        });
    }

    // Obtener una factura por ID (con todas sus relaciones)
    async findById(id) {
        return await Invoice.findByPk(id, {
            include: [
                {
                    model: User,
                    as: 'user'
                },
                {
                    model: InvoiceItem,
                    as: 'items',
                    include: [{
                        model: Product,
                        as: 'product'
                    }]
                }
            ]
        });
    }

    // Crear una factura
    async create(invoiceData) {
        return await Invoice.create(invoiceData);
    }

    // Obtener facturas de un usuario especifico
    async findByUser(userId) {
        return await Invoice.findAll({
            where: { user_id: userId },
            include: [
                {
                    model: InvoiceItem,
                    as: 'items',
                    include: [{
                        model: Product,
                        as: 'product'
                    }]
                }
            ]
        });
    }
}

module.exports = new InvoiceRepository();

Nota sobre las Relaciones

Con Sequelize, las relaciones se cargan automáticamente usando include. No necesitas escribir JOINs manualmente como en SQL directo.

Mejores Prácticas y Consejos

1. Manejo de Errores

  • Siempre usa try/catch en funciones async
  • Devuelve códigos de estado HTTP apropiados (200, 201, 400, 404, 500)
  • Incluye mensajes de error descriptivos
  • Nunca expongas detalles internos de la DB al cliente

2. Validaciones

  • Valida datos en la capa de servicios
  • Valida tipos, rangos y formatos
  • Usa librerías como joi o yup para validaciones complejas
  • No confíes en validaciones del cliente (siempre valida en el servidor)

3. Seguridad

  • Usa prepared statements ($1, $2) para evitar SQL injection
  • Sanitiza inputs del usuario
  • Usa variables de entorno para credenciales (no las hardcodees)
  • Implementa autenticación y autorización (JWT, sesiones)

4. Performance

  • Usa connection pooling (ya incluido en este proyecto)
  • Limita los campos que cargas con attributes
  • Usa índices en la base de datos para búsquedas frecuentes
  • Implementa paginación para listas largas

5. Testing

  • Escribe tests unitarios para servicios
  • Usa mocks para los repositorios en tests
  • Implementa tests de integración para la API completa
  • Usa herramientas como Jest o Mocha

Ejercicios Propuestos

Para reforzar tu aprendizaje, te propongo los siguientes ejercicios:

Ejercicio 1: Agregar Categorías

Crea una nueva entidad Category y relaciónala con Product (1:N):
  • Crea la tabla categories
  • Crea el modelo Sequelize
  • Implementa repository, service y routes completos
  • Agrega un campo category_id en products

Ejercicio 2: Validaciones Avanzadas

Mejora las validaciones del ProductService:
  • Valida que el SKU tenga un formato específico (ej: XXX-XXX-###)
  • Verifica que no exista un producto con el mismo SKU al crear
  • Implementa validación de stock mínimo
  • Agrega logs de todas las operaciones

Ejercicio 3: Reportes de Ventas

Crea endpoints para reportes:
  • Total de ventas por usuario
  • Productos más vendidos
  • Ingresos totales en un rango de fechas
  • Implementa las queries tanto en SQL directo como en Sequelize

Ejercicio 4: Actualización de Stock

Implementa lógica para actualizar el stock automáticamente:
  • Al crear una factura, reduce el stock de los productos
  • Valida que haya stock suficiente antes de crear la factura
  • Implementa una transacción para garantizar consistencia
  • Agrega un endpoint para reponer stock

Ejercicio 5: Migraciones con Sequelize

Aprende a usar migraciones: Ver guía de Migraciones
  • Instala sequelize-cli
  • Crea migraciones para todas las tablas
  • Implementa seeders para datos de prueba
  • Documenta el proceso de setup usando migraciones

Recursos Adicionales

Documentación Oficial

Temas para Profundizar

  • Autenticación con JWT
  • Tests con Jest y Supertest
  • Documentación con Swagger
  • Migraciones y Seeders
  • Deploy en producción (Railway, Render)
  • CI/CD con GitHub Actions

Conclusión

Felicitaciones! Has aprendido a construir una API REST completa con arquitectura en capas. Este proyecto te ha enseñado:
  • Arquitectura en capas (Controllers, Services, Repositories)
  • SQL directo vs ORM (Sequelize)
  • Relaciones entre tablas (1:N y N:M)
  • Docker para desarrollo local
  • Endpoints REST (GET, POST, PUT, DELETE)
  • Buenas prácticas y código limpio

Próximos Pasos

Para continuar tu aprendizaje:
  • 1. Clona el repositorio y experimenta con el código
  • 2. Completa los ejercicios propuestos
  • 3. Agrega autenticación con JWT
  • 4. Escribe tests para tu código
  • 5. Deploya tu API en producción
Express.jsPostgreSQLSequelizeDockerArquitectura en CapasAPI REST

Posts que podrian interesarte