Arquitectura MVC
Vamos a organizar el código con el patrón Model-View-Controller adaptado para APIs.
MVC para APIs
Rutas → Controllers → Services → Models
- Rutas: Definen endpoints
- Controllers: Reciben requests y envían respuestas
- Services: Lógica de negocio
- Models: Acceso a base de datos (Prisma)
Controller
typescript
// src/controllers/product.controller.tsimport { Request, Response } from 'express';import { ProductService } from '../services/product.service'; const productService = new ProductService(); export class ProductController { async index(req: Request, res: Response) { const products = await productService.findAll(); return res.json(products); } async show(req: Request, res: Response) { const { id } = req.params; const product = await productService.findById(id); if (!product) { return res.status(404).json({ error: 'Producto no encontrado' }); } return res.json(product); } async store(req: Request, res: Response) { const product = await productService.create(req.body); return res.status(201).json(product); } async update(req: Request, res: Response) { const { id } = req.params; const product = await productService.update(id, req.body); return res.json(product); } async destroy(req: Request, res: Response) { const { id } = req.params; await productService.delete(id); return res.status(204).send(); }}Service
typescript
// src/services/product.service.tsimport { prisma } from '../config/database'; interface CreateProductDTO { name: string; price: number; description?: string;} export class ProductService { async findAll() { return prisma.product.findMany(); } async findById(id: string) { return prisma.product.findUnique({ where: { id } }); } async create(data: CreateProductDTO) { return prisma.product.create({ data }); } async update(id: string, data: Partial<CreateProductDTO>) { return prisma.product.update({ where: { id }, data, }); } async delete(id: string) { return prisma.product.delete({ where: { id } }); }}Rutas con Controller
typescript
// src/routes/product.routes.tsimport { Router } from 'express';import { ProductController } from '../controllers/product.controller'; const router = Router();const controller = new ProductController(); router.get('/', (req, res) => controller.index(req, res));router.get('/:id', (req, res) => controller.show(req, res));router.post('/', (req, res) => controller.store(req, res));router.put('/:id', (req, res) => controller.update(req, res));router.delete('/:id', (req, res) => controller.destroy(req, res)); export default router;AsyncHandler Helper
typescript
// src/utils/async-handler.tsimport { Request, Response, NextFunction } from 'express'; type AsyncFunction = ( req: Request, res: Response, next: NextFunction) => Promise<any>; export const asyncHandler = (fn: AsyncFunction) => { return (req: Request, res: Response, next: NextFunction) => { Promise.resolve(fn(req, res, next)).catch(next); };}; // Usorouter.get('/', asyncHandler(controller.index));Resumen
- ✅ Controller maneja HTTP
- ✅ Service contiene lógica de negocio
- ✅ Separación clara de responsabilidades
- ✅ Pruebas y mantenimiento más fáciles
Próxima clase: Validación de Datos con Zod! 🚀