Arquitectura Frontend — Atomic Design + Feature-Sliced
La aplicación de Finnova se construye en React Native con un MVP orientado a móvil y la expectativa de extenderse a web en el corto-mediano plazo. Para sostener ese crecimiento sin acumular deuda técnica, el frontend combina dos patrones complementarios más una capa de abstracción de plataforma:
- Feature-Sliced Design (FSD) organiza la lógica de negocio por dominio.
- Atomic Design organiza la UI reutilizable dentro de
shared/ui. - Platform Abstraction Layer aísla las diferencias entre móvil y web.
La decisión y sus alternativas descartadas están documentadas en el ADR: Patrón de diseño — Atomic Design + Feature-Sliced. Este documento describe cómo se aplica esa decisión en la práctica.
Stack del frontend
| Capa | Tecnología | Rol |
|---|---|---|
| Framework | React Native | Base multiplataforma (móvil → web) |
| Navegación | Expo Router | Routing basado en archivos, compatible con web |
| Estado global | Zustand | Estado de UI y de dominio en memoria |
| Data fetching | TanStack Query | Cache, sincronización y estado de servidor |
| Formularios | React Hook Form | Manejo y validación de formularios |
| Lenguaje | TypeScript (strict: true) | Tipado estático en todo el código |
El estado de servidor (datos que vienen del backend) vive en TanStack Query; el estado de cliente (UI, sesión, preferencias) vive en Zustand. No duplicar datos del servidor en Zustand.
Capas y reglas de importación
FSD define capas con una regla de dependencia unidireccional: una capa solo puede importar de capas inferiores. Esto evita dependencias circulares y acoplamiento implícito entre módulos.
pages → features → shared
↑
platform (transversal)
pages/puede importar defeatures/yshared/.features/puede importar deshared/(pero nunca de otra feature).shared/no importa de ninguna otra capa del proyecto.platform/es transversal: provee adaptadores de infraestructura (navegación, storage) y puede ser usado por cualquier capa.
Regla clave: features aisladas
Una feature nunca importa directamente de otra feature. Si dos features necesitan compartir algo (un tipo, un componente, un helper), ese código sube a shared/. Esto mantiene cada dominio como un módulo independiente y testeable de forma aislada.
Estructura de carpetas
src/
├── shared/ # Código reutilizable SIN lógica de negocio
│ ├── ui/ # Componentes genéricos (Atomic Design)
│ │ ├── atoms/
│ │ ├── molecules/
│ │ └── organisms/
│ ├── lib/ # Utilidades, helpers, hooks genéricos
│ └── api/ # Cliente HTTP base y configuración
│
├── features/ # Módulos por dominio de negocio
│ ├── auth/
│ │ ├── ui/ # Componentes y pantallas del módulo
│ │ ├── model/ # Estado (Zustand), lógica de negocio, tipos
│ │ └── api/ # Llamadas al backend de este módulo
│ ├── leaderboard/
│ ├── fdc/ # Financial Data Connection
│ ├── investments/
│ ├── accounting/
│ ├── rewards/
│ ├── courses/
│ └── subscription/
│ ├── ui/
│ ├── model/
│ └── api/
│
├── pages/ # Pantallas raíz (entry points de navegación)
│ ├── HomeScreen.tsx
│ └── ...Screen.tsx
│
└── platform/ # Adaptadores de infraestructura nativa
├── navigation/ # Configuración de Expo Router / React Navigation
└── storage/ # Persistencia local (AsyncStorage, etc.)
Estructura interna de una feature
Cada feature replica las tres mismas sub-capas (FSD a nivel de slice):
| Sub-capa | Responsabilidad | Ejemplos |
|---|---|---|
ui/ | Componentes y pantallas propias del dominio | LoginForm, BalanceCard |
model/ | Estado (Zustand), lógica de negocio, tipos del dominio | useAuthStore, IUser, validaciones |
api/ | Llamadas al backend de este módulo (hooks de TanStack Query) | useLogin, fetchTransactions |
Atomic Design en shared/ui
Los componentes genéricos y sin lógica de negocio viven en shared/ui y siguen la jerarquía de Atomic Design. Un componente que conoce reglas de negocio no va aquí: va en la ui/ de la feature correspondiente.
| Nivel | Definición | Ejemplos |
|---|---|---|
| Atoms | Componente mínimo, sin lógica de negocio. | Button, Input, Text, Icon |
| Molecules | Combinación de atoms con lógica simple de presentación. | LabeledInput, SearchField |
| Organisms | Bloques complejos reutilizables compuestos de molecules/atoms. | AppHeader, FormCard |
Criterio de ubicación: si el componente es agnóstico de dominio y reutilizable en cualquier pantalla →
shared/ui. Si conoce reglas de negocio (p. ej. unTransactionRowque formatea según el tipo de movimiento) →features/<dominio>/ui.
Platform Abstraction Layer
El objetivo es que más del 80% del código (lógica de negocio, estado y UI base) sea reutilizable entre móvil y web sin modificación. Para lograrlo, las diferencias de plataforma no se dispersan con if (Platform.OS === ...) por toda la base de código: se concentran en platform/.
platform/navigation/— configuración de navegación (Expo Router) por plataforma.platform/storage/— persistencia local:AsyncStorageen móvil, equivalentes en web.
Cuando se agregue soporte web, solo habrá que implementar las interfaces de platform/, sin tocar las features ni shared/.
Flujo de datos típico
Un flujo de lectura de datos (p. ej. mostrar el dashboard financiero) atraviesa las capas así:
- TanStack Query maneja cache, reintentos y estado de carga/error.
shared/api/httpClientcentraliza base URL, headers y la inyección del JWT (ver Control de Sesiones).- El componente de UI solo consume
data / isLoading / error; no conoce detalles de transporte.
Convenciones aplicadas
- Nomenclatura: componentes en
PascalCase(BalanceCard.tsx); el resto de archivos/carpetas encamelCase. Ver Convenciones de Código. - Código en inglés, comentarios en español.
- TypeScript
strict, prohibidoanysalvo excepción documentada. - Sin enforcement automático, las reglas de importación entre capas dependen de la disciplina del equipo (idealmente reforzadas con
eslint-plugin-import).