Repositorio del Proyecto
Una nota antes de empezar: Seré honesto contigo. No soy la persona más fanática de la arquitectura en capas. En proyectos reales, siempre opto por arquitectura hexagonal (o puertos y adaptadores), que ofrece mayor flexibilidad y testabilidad.
Pero, 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 estás comenzando, y te prepara para conceptos más avanzados más adelante. Piensa en esto como aprender a caminar antes de correr. Una vez 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
- Relaciones entre tablas (1:N, N:M)
- Docker para desarrollo local
- Buenas prácticas de código limpio
Stack Tecnológico
De arriba hacia abajo: cómo se conectan las tecnologías
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
Arquitectura en Capas
¿Por qué estas tecnologías?
¿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
- 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
1mkdir express-architecture
2cd express-architecture
3npm init -y2. Instalar Dependencias
1npm install express pg sequelizeDonde:
express- Framework web para Node.jspg- Driver de PostgreSQL para SQL directosequelize- 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:
1version: '3.8'
2
3services:
4 postgres:
5 image: postgres:16-alpine
6 container_name: express_002_postgres
7 environment:
8 POSTGRES_USER: postgres
9 POSTGRES_PASSWORD: postgres
10 POSTGRES_DB: products_db
11 ports:
12 - "5433:5432" # Puerto personalizado para evitar conflictos
13 volumes:
14 - postgres_data:/var/lib/postgresql/data
15 - ./init.sql:/docker-entrypoint-initdb.d/init.sql
16
17volumes:
18 postgres_data:¿Por qué puerto 5433?
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:
1-- Tabla de Usuarios
2CREATE TABLE users (
3 id SERIAL PRIMARY KEY,
4 name VARCHAR(100) NOT NULL,
5 email VARCHAR(100) UNIQUE NOT NULL,
6 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
7);
8
9-- Tabla de Productos
10CREATE TABLE products (
11 id SERIAL PRIMARY KEY,
12 description VARCHAR(200) NOT NULL,
13 price DECIMAL(10,2) NOT NULL,
14 stock INTEGER DEFAULT 0,
15 sku VARCHAR(50) UNIQUE NOT NULL,
16 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
17);
18
19-- Tabla de Facturas
20CREATE TABLE invoices (
21 id SERIAL PRIMARY KEY,
22 user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
23 total DECIMAL(10,2) NOT NULL,
24 invoice_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
25 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
26);
27
28-- Tabla débil: Items de Factura (relación N:M)
29CREATE TABLE invoice_items (
30 id SERIAL PRIMARY KEY,
31 invoice_id INTEGER REFERENCES invoices(id) ON DELETE CASCADE,
32 product_id INTEGER REFERENCES products(id) ON DELETE CASCADE,
33 quantity INTEGER NOT NULL,
34 unit_price DECIMAL(10,2) NOT NULL,
35 subtotal DECIMAL(10,2) NOT NULL,
36 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
37);
38
39-- Datos de ejemplo
40INSERT INTO users (name, email) VALUES
41('Jorge Saavedra', 'jorge@example.com'),
42('Ana García', 'ana@example.com');
43
44INSERT INTO products (description, price, stock, sku) VALUES
45('Laptop Dell XPS', 1299.99, 10, 'LAP-DELL-001'),
46('Mouse Logitech', 29.99, 50, 'MOU-LOG-002'),
47('Teclado Mecánico', 89.99, 25, 'KEY-MEC-003');5. Iniciar PostgreSQL
1# Iniciar PostgreSQL
2docker-compose up -d
3
4# Verificar que esté corriendo
5docker ps
6
7# Conectarse a la base de datos (opcional)
8docker exec -it express_002_postgres psql -U postgres -d products_dbSQL 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?
| Característica | SQL Directo (pg) | ORM (Sequelize) |
|---|---|---|
| Control | ✓ Total control sobre queries | ⚬ Abstracción puede limitar |
| Curva de aprendizaje | ⚬ Necesitas saber SQL | ✓ Más fácil de empezar |
| Productividad | ⚬ Más código manual | ✓ Menos código, más rápido |
| Debugging | ✓ Ves exactamente el SQL | ⚬ Queries generadas automáticamente |
| Migraciones | ⚬ Manual | ✓ Automáticas |
| Relaciones | ⚬ JOINs manuales | ✓ Includes automáticos |
| Casos de uso | Queries complejas, optimización | CRUD rápido, prototipado |
Característica
SQL Directo (pg)
ORM (Sequelize)
Característica
SQL Directo (pg)
ORM (Sequelize)
Característica
SQL Directo (pg)
ORM (Sequelize)
Característica
SQL Directo (pg)
ORM (Sequelize)
Característica
SQL Directo (pg)
ORM (Sequelize)
Característica
SQL Directo (pg)
ORM (Sequelize)
Característica
SQL Directo (pg)
ORM (Sequelize)
Ejemplo Comparativo: Obtener Todos los Productos
SQL Directo
1// productRepository.js
2class ProductRepository {
3 constructor(pool) {
4 this.pool = pool;
5 }
6
7 async findAll() {
8 const result = await this.pool.query(
9 'SELECT * FROM products ORDER BY id'
10 );
11 return result.rows;
12 }
13
14 async findById(id) {
15 const result = await this.pool.query(
16 'SELECT * FROM products WHERE id = $1',
17 [id]
18 );
19 return result.rows[0];
20 }
21}ORM (Sequelize)
1// productRepositoryORM.js
2class ProductRepositoryORM {
3 constructor(model) {
4 this.model = model;
5 }
6
7 async findAll() {
8 return await this.model.findAll({
9 order: [['id', 'ASC']]
10 });
11 }
12
13 async findById(id) {
14 return await this.model.findByPk(id);
15 }
16}Como puedes ver, con SQL directo escribes la query completa, mientras que conSequelize 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
1const { Sequelize } = require('sequelize');
2
3const sequelize = new Sequelize('products_db', 'postgres', 'postgres', {
4 host: 'localhost',
5 port: 5433,
6 dialect: 'postgres',
7 logging: false, // Desactiva los logs SQL en consola
8 pool: {
9 max: 5,
10 min: 0,
11 acquire: 30000,
12 idle: 10000
13 }
14});
15
16// Probar la conexión
17async function testConnection() {
18 try {
19 await sequelize.authenticate();
20 console.log('✓ Conexión a PostgreSQL exitosa');
21 } catch (error) {
22 console.error('❌ Error conectando a PostgreSQL:', error);
23 }
24}
25
26testConnection();
27
28module.exports = sequelize;2. Modelo de Producto
1const { DataTypes } = require('sequelize');
2const sequelize = require('../database');
3
4const Product = sequelize.define('Product', {
5 id: {
6 type: DataTypes.INTEGER,
7 primaryKey: true,
8 autoIncrement: true
9 },
10 description: {
11 type: DataTypes.STRING(200),
12 allowNull: false
13 },
14 price: {
15 type: DataTypes.DECIMAL(10, 2),
16 allowNull: false
17 },
18 stock: {
19 type: DataTypes.INTEGER,
20 defaultValue: 0
21 },
22 sku: {
23 type: DataTypes.STRING(50),
24 allowNull: false,
25 unique: true
26 }
27}, {
28 tableName: 'products',
29 timestamps: true,
30 createdAt: 'created_at',
31 updatedAt: false
32});
33
34module.exports = Product;Sincronización de Modelos
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
Leyenda:
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.
1// En associations.js
2User.hasMany(Invoice, {
3 foreignKey: 'user_id',
4 as: 'invoices'
5});
6
7Invoice.belongsTo(User, {
8 foreignKey: 'user_id',
9 as: 'user'
10});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).
1// En associations.js
2Invoice.belongsToMany(Product, {
3 through: InvoiceItem,
4 foreignKey: 'invoice_id',
5 otherKey: 'product_id',
6 as: 'products'
7});
8
9Product.belongsToMany(Invoice, {
10 through: InvoiceItem,
11 foreignKey: 'product_id',
12 otherKey: 'invoice_id',
13 as: 'invoices'
14});¿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 →
1// Obtener una factura con todos sus datos relacionados
2const invoice = await Invoice.findByPk(1, {
3 include: [
4 {
5 model: User,
6 as: 'user',
7 attributes: ['id', 'name', 'email']
8 },
9 {
10 model: InvoiceItem,
11 as: 'items',
12 include: [{
13 model: Product,
14 as: 'product'
15 }]
16 }
17 ]
18});
19
20// Resultado:
21{
22 id: 1,
23 total: 1419.97,
24 invoice_date: '2025-01-31T...',
25 user: {
26 id: 1,
27 name: 'Jorge Saavedra',
28 email: 'jorge@example.com'
29 },
30 items: [
31 {
32 quantity: 1,
33 unit_price: 1299.99,
34 subtotal: 1299.99,
35 product: {
36 description: 'Laptop Dell XPS',
37 price: 1299.99,
38 sku: 'LAP-DELL-001'
39 }
40 },
41 {
42 quantity: 4,
43 unit_price: 29.99,
44 subtotal: 119.98,
45 product: {
46 description: 'Mouse Logitech',
47 price: 29.99,
48 sku: 'MOU-LOG-002'
49 }
50 }
51 ]
52}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
1const { Pool } = require('pg');
2
3class ProductRepository {
4 constructor() {
5 this.pool = new Pool({
6 user: 'postgres',
7 host: 'localhost',
8 database: 'products_db',
9 password: 'postgres',
10 port: 5433,
11 });
12 }
13
14 async findAll() {
15 const result = await this.pool.query(
16 'SELECT * FROM products ORDER BY id'
17 );
18 return result.rows;
19 }
20
21 async findById(id) {
22 const result = await this.pool.query(
23 'SELECT * FROM products WHERE id = $1',
24 [id]
25 );
26 return result.rows[0];
27 }
28
29 async create(productData) {
30 const { description, price, stock, sku } = productData;
31 const result = await this.pool.query(
32 `INSERT INTO products (description, price, stock, sku)
33 VALUES ($1, $2, $3, $4)
34 RETURNING *`,
35 [description, price, stock, sku]
36 );
37 return result.rows[0];
38 }
39
40 async update(id, productData) {
41 const { description, price, stock, sku } = productData;
42 const result = await this.pool.query(
43 `UPDATE products
44 SET description = $1, price = $2, stock = $3, sku = $4
45 WHERE id = $5
46 RETURNING *`,
47 [description, price, stock, sku, id]
48 );
49 return result.rows[0];
50 }
51
52 async delete(id) {
53 await this.pool.query('DELETE FROM products WHERE id = $1', [id]);
54 return true;
55 }
56}
57
58module.exports = new ProductRepository();Repositorio con Sequelize (ORM)
1const Product = require('../models/Product');
2
3class ProductRepositoryORM {
4 async findAll() {
5 return await Product.findAll({
6 order: [['id', 'ASC']]
7 });
8 }
9
10 async findById(id) {
11 return await Product.findByPk(id);
12 }
13
14 async create(productData) {
15 return await Product.create(productData);
16 }
17
18 async update(id, productData) {
19 const product = await Product.findByPk(id);
20 if (!product) return null;
21
22 return await product.update(productData);
23 }
24
25 async delete(id) {
26 const product = await Product.findByPk(id);
27 if (!product) return false;
28
29 await product.destroy();
30 return true;
31 }
32}
33
34module.exports = new ProductRepositoryORM();Firma de Métodos Idéntica
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.
1// Puedes cambiar entre ambos repositorios aquí:
2const productRepository = require('../repositories/productRepositoryORM');
3// const productRepository = require('../repositories/productRepository');
4
5class ProductService {
6 async getAllProducts() {
7 try {
8 return await productRepository.findAll();
9 } catch (error) {
10 throw new Error('Error obteniendo productos: ' + error.message);
11 }
12 }
13
14 async getProductById(id) {
15 // Validación de negocio
16 if (!id || isNaN(id)) {
17 throw new Error('ID de producto inválido');
18 }
19
20 const product = await productRepository.findById(id);
21
22 if (!product) {
23 throw new Error(`Producto con ID ${id} no encontrado`);
24 }
25
26 return product;
27 }
28
29 async createProduct(productData) {
30 // Validaciones de negocio
31 if (!productData.description || !productData.price || !productData.sku) {
32 throw new Error('Faltan campos obligatorios');
33 }
34
35 if (productData.price <= 0) {
36 throw new Error('El precio debe ser mayor a 0');
37 }
38
39 if (productData.stock < 0) {
40 throw new Error('El stock no puede ser negativo');
41 }
42
43 return await productRepository.create(productData);
44 }
45
46 async updateProduct(id, productData) {
47 // Verificar que el producto existe
48 await this.getProductById(id);
49
50 // Validaciones de negocio
51 if (productData.price && productData.price <= 0) {
52 throw new Error('El precio debe ser mayor a 0');
53 }
54
55 if (productData.stock && productData.stock < 0) {
56 throw new Error('El stock no puede ser negativo');
57 }
58
59 return await productRepository.update(id, productData);
60 }
61
62 async deleteProduct(id) {
63 // Verificar que el producto existe
64 await this.getProductById(id);
65
66 return await productRepository.delete(id);
67 }
68}
69
70module.exports = new ProductService();Separación de Responsabilidades
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.
1const express = require('express');
2const router = express.Router();
3const productService = require('../services/productService');
4
5// GET /api/products - Obtener todos los productos
6router.get('/', async (req, res) => {
7 try {
8 const products = await productService.getAllProducts();
9 res.json(products);
10 } catch (error) {
11 res.status(500).json({
12 error: error.message
13 });
14 }
15});
16
17// GET /api/products/:id - Obtener un producto por ID
18router.get('/:id', async (req, res) => {
19 try {
20 const product = await productService.getProductById(req.params.id);
21 res.json(product);
22 } catch (error) {
23 res.status(404).json({
24 error: error.message
25 });
26 }
27});
28
29// POST /api/products - Crear un nuevo producto
30router.post('/', async (req, res) => {
31 try {
32 const newProduct = await productService.createProduct(req.body);
33 res.status(201).json(newProduct);
34 } catch (error) {
35 res.status(400).json({
36 error: error.message
37 });
38 }
39});
40
41// PUT /api/products/:id - Actualizar un producto
42router.put('/:id', async (req, res) => {
43 try {
44 const updatedProduct = await productService.updateProduct(
45 req.params.id,
46 req.body
47 );
48 res.json(updatedProduct);
49 } catch (error) {
50 res.status(400).json({
51 error: error.message
52 });
53 }
54});
55
56// DELETE /api/products/:id - Eliminar un producto
57router.delete('/:id', async (req, res) => {
58 try {
59 await productService.deleteProduct(req.params.id);
60 res.status(204).send();
61 } catch (error) {
62 res.status(404).json({
63 error: error.message
64 });
65 }
66});
67
68module.exports = router;Servidor Principal
1const express = require('express');
2const productRoutes = require('./routes/productRoutes');
3
4const app = express();
5const PORT = 4000;
6
7// Middleware
8app.use(express.json());
9
10// Routes
11app.use('/api/products', productRoutes);
12
13// Servidor
14app.listen(PORT, () => {
15 console.log(`🚀 Servidor corriendo en http://localhost:${PORT}`);
16 console.log(`Endpoints disponibles:`);
17 console.log(` GET /api/products`);
18 console.log(` GET /api/products/:id`);
19 console.log(` POST /api/products`);
20 console.log(` PUT /api/products/:id`);
21 console.log(` DELETE /api/products/:id`);
22});Probando la API
1. Iniciar el Servidor
1# Terminal 1: Asegúrate de que PostgreSQL esté corriendo
2docker-compose up -d
3
4# Terminal 2: Inicia el servidor Express
5npm start2. Probar con cURL
Obtener todos los productos
1curl http://localhost:4000/api/productsObtener un producto por ID
1curl http://localhost:4000/api/products/1Crear un nuevo producto
1curl -X POST http://localhost:4000/api/products \
2 -H "Content-Type: application/json" \
3 -d '{
4 "description": "Monitor LG 27 pulgadas",
5 "price": 299.99,
6 "stock": 15,
7 "sku": "MON-LG-005"
8 }'Actualizar un producto
1curl -X PUT http://localhost:4000/api/products/1 \
2 -H "Content-Type: application/json" \
3 -d '{
4 "price": 1199.99,
5 "stock": 8
6 }'Eliminar un producto
1curl -X DELETE http://localhost:4000/api/products/1Usando Postman o Thunder Client
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)
1const { Invoice, User, InvoiceItem, Product } = require('../models');
2
3class InvoiceRepository {
4 // Obtener todas las facturas con usuario e items
5 async findAll() {
6 return await Invoice.findAll({
7 include: [
8 {
9 model: User,
10 as: 'user',
11 attributes: ['id', 'name', 'email']
12 },
13 {
14 model: InvoiceItem,
15 as: 'items',
16 include: [{
17 model: Product,
18 as: 'product'
19 }]
20 }
21 ],
22 order: [['id', 'DESC']]
23 });
24 }
25
26 // Obtener una factura por ID (con todas sus relaciones)
27 async findById(id) {
28 return await Invoice.findByPk(id, {
29 include: [
30 {
31 model: User,
32 as: 'user'
33 },
34 {
35 model: InvoiceItem,
36 as: 'items',
37 include: [{
38 model: Product,
39 as: 'product'
40 }]
41 }
42 ]
43 });
44 }
45
46 // Crear una factura
47 async create(invoiceData) {
48 return await Invoice.create(invoiceData);
49 }
50
51 // Obtener facturas de un usuario específico
52 async findByUser(userId) {
53 return await Invoice.findAll({
54 where: { user_id: userId },
55 include: [
56 {
57 model: InvoiceItem,
58 as: 'items',
59 include: [{
60 model: Product,
61 as: 'product'
62 }]
63 }
64 ]
65 });
66 }
67}
68
69module.exports = new InvoiceRepository();Nota sobre las Relaciones
include. No necesitas escribir JOINs manualmente como en SQL directo.Mejores Prácticas y Consejos
1. Manejo de Errores
- Siempre usa
try/catchen funcionesasync - 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
joioyuppara 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 relacionala con Product (1:N):
- • Crea la tabla
categories - • Crea el modelo Sequelize
- • Implementa repository, service y routes completos
- • Agrega un campo
category_idenproducts
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:
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