Portafolio profesional

Construyendo un CMS Backend para Portafolios con NestJS y PostgreSQL

Cómo diseñé una API modular de CMS para portafolios personales, resolviendo problemas de gestión de contenido manual y escalabilidad con TypeScript, Prisma y Docker.

Artículo

# Construyendo el backend de mi portafolio: un CMS modular con NestJS y Prisma ## El problema Como desarrollador backend, necesitaba un portafolio que fuera más que una página estática. Quería poder actualizar proyectos, agregar nuevas tecnologías y recibir mensajes de contacto sin tener que tocar código cada vez. El proceso manual de editar archivos HTML o JSON se volvía tedioso, especialmente cuando quería mostrar métricas dinámicas como estrellas de GitHub o estado de proyectos. Además, necesitaba un sistema que pudiera escalar si decidía agregar un blog técnico o gestionar múltiples tipos de contenido. ## Por qué NestJS + Prisma Podría haber usado Express directamente -es más ligero y tengo experiencia con él- pero para un CMS que maneja múltiples recursos con relaciones complejas (proyectos ↔ tecnologías, artículos ↔ etiquetas), necesitaba una estructura que me obligara a ser organizado desde el principio. NestJS me dio eso: módulos separados, inyección de dependencias nativa y decoradores que hacen el código más declarativo. Prisma fue la elección natural sobre TypeORM o Sequelize porque su cliente generado automáticamente me ahorraba escribir interfaces manualmente para cada modelo. Con proyectos que tienen tecnologías asociadas y artículos con traducciones, tener un esquema centralizado en `schema.prisma` y tipos TypeScript generados automáticamente redujo errores y tiempo de desarrollo. Además, las migraciones son más predecibles. ## Arquitectura El sistema sigue una arquitectura por capas clásica pero con algunos twists específicos para un CMS: ```mermaid flowchart LR Client["Browser/Postman Frontend o pruebas"] -- HTTP request --> Gateway["NestJS Controllers Rutas API v1"] Gateway -- valida y transforma --> Services["Services Layer Lógica de negocio"] Services -- queries --> DB["PostgreSQL Prisma Client"] Services -- envía --> Mailer["Nodemailer Envío de emails"] Services -- escribe logs --> Analytics["PostgreSQL Tablas de métricas"] DB -- seed inicial --> Seed["prisma/seed.ts Datos iniciales"] NOTE["Evidence gap: No hay evidencia de colas de mensajes o caché"] ``` El flujo es directo: las peticiones HTTP llegan a los controladores de NestJS, que validan los DTOs usando `class-validator`. Luego delegan a la capa de servicios donde ocurre la lógica real -crear proyectos, asociar tecnologías, procesar contactos- y finalmente usan el Prisma Client para persistir en PostgreSQL. Un punto interesante es que las métricas de solicitudes y logs del servidor también van a tablas dedicadas en la misma base de datos, no a un sistema externo. **Decisión 1: Una sola base de datos para todo** Podría haber separado las métricas y logs en otra DB o usar un servicio como Datadog, pero para un portafolio personal, mantener todo en PostgreSQL simplifica el despliegue y los backups. Las tablas `RequestLog` y `ServerMetrics` conviven con `Project` y `ContactMessage`. El trade-off es potencial impacto en performance, pero para el volumen esperado es aceptable. **Decisión 2: Seed automático con Prisma** Incluí un script `prisma/seed.ts` que pobla la base de datos con proyectos de ejemplo y un usuario admin. Esto es crucial para demostraciones y desarrollo local. En producción, este seed solo corre en el primer deploy o cuando se indica explícitamente. **Decisión 3: Validación estricta con class-validator** Cada endpoint usa DTOs con decoradores como `@IsString()`, `@IsOptional()`, `@IsArray()`. NestJS tiene un pipe global `ValidationPipe` configurado con `whitelist: true` y `forbidNonWhitelisted: true`, lo que significa que si el cliente manda campos extra, la API los rechaza. Esto previene ataques de polución y asegura consistencia. ## El trade-off que nadie te cuenta El mayor trade-off fue **decidir cómo manejar las traducciones para proyectos y artículos**. Inicialmente diseñé una tabla `Translation` separada con `locale` y `text`, y relaciones muchos-a-muchos con `Project` y `Article`. Esto era elegante en teoría pero engorroso en la práctica: cada consulta requería joins complejos, y crear un proyecto nuevo implicaba múltiples inserciones. Después de varias iteraciones (mirá las migraciones en `prisma/migrations/20260501204229_remove_translations` y `20260710172923_add_i18n_to_articles_and_diagrams`), terminé con una solución híbrida: uso un DTO `I18nTextDto` que se almacena como JSON en la base de datos. Por ejemplo: ```typescript // src/common/dto/i18n-text.dto.ts export class I18nTextDto { @IsObject() translations: Record<string, string>; // { 'es': '...', 'en': '...' } } ``` Esto simplifica enormemente las consultas -solo fetch del campo JSON- pero pierde la capacidad de hacer búsquedas eficientes por texto en un idioma específico a nivel de base de datos. Para mi caso de uso, donde el contenido es gestionado por mí y no por miles de usuarios, fue un trade-off aceptable. Si necesitara búsqueda full-text multilingüe, reconsideraría la decisión. ## Resultado - **API completa con 10+ endpoints** para proyectos, contactos, medios y métricas, documentada automáticamente con Swagger en `/api/v1/docs`. - **Sistema de contactos con auto-respuesta**: cuando alguien envía un mensaje, recibe un email de confirmación y yo recibo una notificación, todo usando plantillas Handlebars. - **Métricas integradas**: cada request se loguea en la DB con timestamp, método, ruta y status code, lo que me permite analizar tráfico sin herramientas externas. - **Despliegue con Docker**: un `docker-compose.yml` levanta PostgreSQL y la app en segundos, ideal para desarrollo y producción en VPS. ## Lo que haría diferente Separaría más claramente la lógica de negocio de la infraestructura. Algunos servicios como `ProjectService` manejan validación, transformación de datos, y llamadas a Prisma. En un proyecto más grande, dividiría en `ProjectRepository` (solo acceso a DB), `ProjectValidator` (reglas de negocio) y `ProjectService` (orquestación). También consideraría usar `bull` o `@nestjs/bull` para colas de email asíncronas, aunque para el volumen actual no es crítico. --- ¿Querés profundizar en algún componente? Contactame o revisá el código en https://github.com/Albarracin-sg/BACKEND-PORTAFOLIO.