Arquitectura del Frontend
El proyecto frontend utiliza una combinación de Clean Architecture con Vertical Slicing, organizando el código por funcionalidades en lugar de por tipo de archivo. Esto evita archivos de configuración gigantes y mantiene cada módulo cohesivo e independiente.
Además, se implementa i18n para el manejo de traducciones multilenguaje.
Estructura General de Carpetas
- identity.routes.ts
Convención de Nombres de Módulos
Los nombres de las carpetas de funcionalidades deben coincidir exactamente con los nombres usados en el backend (Spring Boot). Esto facilita la ubicación del código entre ambos entornos y reduce la fricción al navegar entre proyectos.
Por ejemplo, si en el backend existe el paquete identity, la carpeta en el frontend también debe llamarse identity.
Descripción de Cada Capa
Core
Contiene todo lo que es base o fundacional para la aplicación: piezas que no funcionan de forma independiente pero que otros módulos necesitan para operar.
Ejemplos de lo que vive en core:
- Componentes base/abstractos: componentes genéricos que no se usan directamente, sino como base para componentes específicos. Por ejemplo, un
GenericPickerComponentque sirve de base para unAlmacenPickerComponento unProveedorPickerComponent. - Interceptores de red globales: interceptores HTTP que aplican a toda la aplicación (no a una funcionalidad en específico).
- Providers y configuración de arranque: setup de i18n, configuración de TanStack Query, tokens de inyección globales, etc.
Si algo en core empieza a usarse solo dentro de una funcionalidad específica, es señal de que debe moverse a esa feature o a shared/.
Componentes
Elementos visuales que pertenecen a una funcionalidad y se reutilizan en múltiples páginas dentro de la misma (ej. un AvatarComponent que aparece en el perfil y en el menú lateral).
Si un componente es exclusivo de una sola página y no se compartirá con otras, debe ubicarse anidado dentro de la carpeta de esa página (pages/login/components/). Esto optimiza la carga y mantiene la cohesión.
Modelos
Representan la estructura de datos del módulo. Inicialmente son un reflejo directo del domain/model de JPA definido en el backend.
// Ejemplo: reflejo del dominio JPA WebUsuario
export interface WebUsuario {
id: number;
usuario: string;
activo: boolean;
locale: string;
rol: WebRol;
tipoUsuario: TipoUsuario;
createdAt: string;
fechaPassword: string;
}Servicios
Se encargan exclusivamente de la lógica de negocio y las peticiones HTTP. Cada funcionalidad tiene sus propios servicios (ej. authService, roleService).
Queries (TanStack Query para Angular)
Se utiliza TanStack Query para el manejo asíncrono de datos y peticiones. Sus principales ventajas son:
- Reutilizar y deduplicar solicitudes idénticas.
- Sincronizar el estado entre múltiples componentes.
- Manejar cachés, temporizadores e invalidaciones programáticamente.
- Realizar actualizaciones optimistas: actualizar la UI antes de que la petición en segundo plano finalice.
Se deben definir Keys (llaves) centralizadas para gestionar todas las consultas. Esto evita inconsistencias y facilita la invalidación de caché.
// Ejemplo de keys centralizadas
export const identityKeys = {
all: ['identity'] as const,
users: () => [...identityKeys.all, 'users'] as const,
user: (id: number) => [...identityKeys.users(), id] as const,
}Store (Estado Global)
El Store se usa cuando hay estado propio alrededor de una consulta o funcionalidad, más allá del simple resultado de la petición.
La distinción clave frente a las Queries es:
| Query | Store | |
|---|---|---|
| Propósito | Obtener datos del servidor y cachearlos | Mantener estado derivado o lógica sobre esos datos |
| Ejemplo | Consultar el usuario actual desde la API | Saber si el usuario está autenticado, cuál es su rol activo, o si tiene un permiso específico |
| Cuándo usarlo | Solo necesito el dato y su estado de carga | Necesito lógica, utilidades o estado que no me da la query sola |
Ejemplo concreto con el usuario:
Una query de usuario devuelve el objeto WebUsuario. Pero el store de identidad expone además:
isAuthenticated— boolean derivado de si la query devolvió un usuario válido.currentRol— el rol activo del usuario.hasRole(rol)— utilidad para verificar permisos desde cualquier componente.
Si solo necesitas mostrar datos de una petición, usa una Query. Si necesitas derivar estado o exponer utilidades sobre esos datos, usa el Store.
Directivas
Atributos HTML personalizados para extender el comportamiento del template. Ejemplos:
- Directiva de clasificación: ordena columnas de tablas.
- Directiva de rol: oculta o muestra elementos según los permisos del usuario autenticado.
Guards (Guardias de Rutas)
Controlan el acceso a las rutas de la aplicación. Tipos comunes:
| Guard | Propósito |
|---|---|
authGuard | Permite acceso solo a usuarios autenticados |
guestGuard | Permite acceso solo a usuarios no autenticados (ej. pantalla de login) |
roleGuard | Restringe el acceso según el rol del usuario |
Interceptores
Intervienen en las peticiones HTTP salientes y las respuestas entrantes:
| Interceptor | Comportamiento |
|---|---|
| Credenciales | Adjunta cookies/tokens a cada petición saliente |
| Sesión expirada | Detecta errores 401 y redirige automáticamente al login |
Rutas
Archivo de definición de rutas públicas y privadas específicas de la funcionalidad. Se importa en el enrutador principal de la aplicación.
// identity.routes.ts
export const identityRoutes: Routes = [
{
path: 'login',
component: LoginComponent,
canActivate: [guestGuard],
},
{
path: 'profile',
component: ProfileComponent,
canActivate: [authGuard],
},
]Carpeta Shared (Compartidos)
Ubicada en la raíz de src/, agrupa todo lo que es genérico y transversal a la aplicación, sin pertenecer a ninguna funcionalidad en específico:
- Componentes reutilizables globales (botones, modales, alertas).
- Mensajes de error estándar.
- Pipes y utilidades comunes.
Si un componente, pipe o utilidad solo lo usa un módulo, va dentro de ese módulo. Si lo usan dos o más módulos, va en shared/.