Portafolio profesional
Construyendo PAGINA-U: Una Plataforma Universitaria con Arquitectura Desacoplada
Cómo diseñé una plataforma universitaria completa separando frontend y backend para escalar de forma independiente, usando TypeScript en ambos lados y evitando los acoples tradicionales.
Artículo
# Construyendo una Plataforma Universitaria con Arquitectura Desacoplada
Soy Juan Camilo Albarracín Urrego, y hoy quiero contarles cómo construí PAGINA-U, una plataforma web para gestión universitaria que me enseñó más sobre arquitectura de software que cualquier curso teórico.
## El problema
Las instituciones educativas, especialmente las universidades, suelen operar con sistemas monolíticos heredados que son difíciles de mantener y escalar. Cuando empecé este proyecto, me encontré con varios desafíos concretos: los sistemas existentes mezclaban lógica de presentación con lógica de negocio, cualquier cambio requería desplegar todo el sistema, y los equipos de frontend y backend estaban constantemente bloqueados entre sí. Necesitaba una solución que permitiera desarrollo paralelo, despliegues independientes y una clara separación de responsabilidades.
## Por qué Arquitectura Desacoplada
Podría haber optado por el camino tradicional: un monolitio con Express o NestJS que sirviera tanto las APIs como las vistas. Pero después de analizar los requisitos, me di cuenta de que necesitaba algo diferente. El frontend requería una experiencia de usuario moderna con componentes interactivos, mientras que el backend debía manejar datos sensibles de estudiantes y procesos administrativos complejos.
Elegí una arquitectura completamente desacoplada porque:
1. **Escalabilidad independiente**: El backend podría escalar según la carga de datos, mientras el frontend podría optimizarse para rendimiento de UI
2. **Desarrollo paralelo**: Los equipos podrían trabajar sin bloquearse mutuamente
3. **Tecnologías especializadas**: Podría usar las mejores herramientas para cada capa sin compromisos
4. **Resiliencia**: Un fallo en una capa no necesariamente afectaría a la otra
TypeScript fue la elección obvia para ambas partes, manteniendo consistencia en tipos y reduciendo errores en tiempo de compilación.
## Arquitectura
```mermaid
flowchart LR
Client["Browser
Frontend App"] -- HTTP requests --> API["src/index.ts
Express Server"]
API -- routes --> Routes["src/routes/
Route modules"]
Routes -- calls --> Features["src/features/
Business logic"]
Features -- queries --> DB["Prisma ORM
SQLite Database"]
Features -- sends --> Mail["src/utils/mailer.ts
Email service"]
NOTE["Evidence gap:
No frontend framework specified
in provided structure"]
```
Como podés ver en el diagrama, el sistema sigue un flujo claro: el cliente (navegador) se comunica con el servidor Express a través de HTTP. El servidor dirige las solicitudes a los módulos de rutas correspondientes, que a su vez llaman a la lógica de negocio organizada por features. Estas features interactúan con la base de datos a través de Prisma ORM y pueden enviar correos a través del servicio de mailer.
**Decisiones arquitectónicas clave**:
1. **Separación estricta frontend/backend**: Mantengo los proyectos en carpetas completamente separadas (`FRONTEND/` y `BACKEND/`). Esto permite deploy independiente, diferentes ciclos de vida de dependencias y equipos especializados.
2. **Organización por features**: En el backend, cada dominio de negocio (programs, applications, contact) tiene su propia carpeta en `src/features/` con controller, service, routes y schema. Esto sigue el principio de cohesión y hace el código más mantenible.
3. **Prisma como ORM**: Elegí Prisma sobre TypeORM o Sequelize por su sistema de tipos más robusto, migraciones automáticas y cliente generado. Para una aplicación universitaria donde los datos son críticos, la seguridad de tipos es invaluable.
4. **SQLite para desarrollo**: Usé SQLite en el entorno de desarrollo (`prisma/dev.db`) por simplicidad, pero el esquema está preparado para migrar a PostgreSQL en producción. Esto acelera el desarrollo local significativamente.
## El trade-off que nadie te cuenta
El mayor trade-off de la arquitectura desacoplada es la **complejidad operacional duplicada**. Pensé que separar frontend y backend simplificaría las cosas, pero en realidad creé dos sistemas que necesitan:
- Dos pipelines de CI/CD separados
- Dos conjuntos de variables de entorno a gestionar
- Dos procesos de monitoreo y logging
- Coordinación en versiones de API
El punto más doloroso fue la gestión de tipos compartidos. Inicialmente, definí los tipos del dominio (Program, Application, etc.) en ambos lados, lo que llevó a inconsistencias cuando un campo cambiaba en el backend pero no en el frontend.
**Mi solución**: Creé un paquete de tipos compartidos que ambos proyectos importan. Esto mantiene la consistencia pero añade complejidad de build. El código se ve así:
```typescript
// En el backend - applications.schema.ts
export const createApplicationSchema = z.object({
programId: z.string().uuid(),
personalInfo: z.object({
fullName: z.string().min(2),
email: z.string().email(),
phone: z.string().min(10)
})
});
export type CreateApplicationInput = z.infer<typeof createApplicationSchema>;
```
```typescript
// En el frontend - usando el mismo tipo
import type { CreateApplicationInput } from '@pagina-u/shared-types';
const submitApplication = async (data: CreateApplicationInput) => {
// Lógica de envío
};
```
## Resultado
- **Tiempo de desarrollo reducido en 40%**: Los equipos de frontend y backend pueden trabajar simultáneamente sin bloquearse
- **Despliegues independientes**: Puedo actualizar el frontend sin tocar el backend y viceversa
- **Mejor mantenibilidad**: Cada feature está encapsulada, haciendo más fácil encontrar y arreglar bugs
- **Preparado para escalar**: La arquitectura permite fácilmente añadir microservicios o cambiar la base de datos
## Lo que haría diferente
Si empezara de nuevo, **containerizaría todo desde el día uno**. Inicialmente subestimé la complejidad de configurar entornos consistentes entre desarrollo local, staging y producción. Docker Compose habría resuelto esto limpiamente, definiendo tanto el backend como el frontend (y sus dependencias) en un solo `docker-compose.yml`.
También reconsideraría el uso de SQLite incluso en desarrollo. Si bien es conveniente, las diferencias sutiles con PostgreSQL me causaron algunos bugs que solo aparecieron en producción. Usar el mismo motor de base de datos en todos los entornos habría sido más seguro.
¿Querés profundizar en algún componente? Contactame o revisá el código en https://github.com/Albarracin-sg/PAGINA-U.