Skip to content

Estructura de Módulos React - Patrón Arquitectónico

Última actualización: 2026-01-29 Estado: Estándar arquitectónico Aplicabilidad: Todos los módulos React del sistema

Descripción

Define la estructura de carpetas estándar para módulos React en Sistema Bautista. Esta organización por recursos y funcionalidades facilita escalabilidad, mantenibilidad y colaboración entre desarrolladores.

Contexto

Problema

Los módulos React sin estructura consistente resultan en:

  • Dificultad para ubicar archivos relacionados
  • Código duplicado entre módulos
  • Onboarding lento de nuevos desarrolladores
  • Refactoring costoso
  • Testing fragmentado

Solución

Estructura modular basada en separación por recursos de negocio con capas bien definidas.

Estructura Base de Módulo

Opción A: Estructura Plana (Módulos Simples)

Para módulos con 1-3 recursos y menos de 15 componentes totales.

ts/{nombre-modulo}/
├── {ModuleName}App.tsx          # Aplicación principal
├── config/                      # Configuración del módulo
│   ├── queryKeys.ts
│   ├── routeHierarchy.ts
│   ├── routes.tsx
│   └── sidebar.ts
├── components/
├── views/
├── hooks/
├── services/
├── schemas/
├── types/
└── mappers/

Opción B: Estructura por Recurso (RECOMENDADA - Módulos Complejos)

Para módulos con 3+ recursos, 15+ componentes. Sigue el principio de Colocation (React 19).

ts/{nombre-modulo}/
├── {ModuleName}App.tsx          # Aplicación principal

├── config/                      # Configuración centralizada del módulo
│   ├── queryKeys.ts            # Query keys para TanStack Query
│   ├── routeHierarchy.ts       # Jerarquía navegación breadcrumbs
│   ├── routes.tsx              # Definición rutas React Router
│   └── sidebar.ts              # Configuración menú lateral

├── {Recurso}/                   # ⭐ AGRUPACIÓN POR RECURSO
│   ├── components/              # Componentes UI del recurso
│   │   ├── {Recurso}Form/
│   │   │   ├── index.tsx       # Componente principal (usa index.tsx)
│   │   │   ├── DatosBasicos.tsx # Sub-componentes sin prefijo redundante
│   │   │   ├── DatosAdicionales.tsx
│   │   │   └── utils.ts        # Helpers específicos del form
│   │   ├── {Recurso}Table/
│   │   │   └── index.tsx
│   │   ├── ModalsGroup/        # Agrupar modales relacionados
│   │   │   ├── AddModal.tsx
│   │   │   ├── EditModal.tsx
│   │   │   └── DeleteConfirmModal.tsx
│   │   └── StandaloneModal.tsx # Modales simples van directos
│   │
│   ├── views/                   # Vistas principales del recurso
│   │   ├── {Recurso}View.tsx   # Lista/tabla principal
│   │   └── {Recurso}FormView.tsx # Vista formulario
│   │
│   ├── hooks/                   # Hooks específicos del recurso
│   │   ├── use{Recurso}.ts     # Query/Mutations principales
│   │   ├── use{Recurso}FormLogic.ts # Lógica negocio específica
│   │   └── use{SubRecurso}.ts  # Sub-recursos relacionados
│   │
│   ├── services/                # Servicios API del recurso
│   │   ├── {recurso}.service.ts
│   │   └── {subRecurso}.service.ts
│   │
│   ├── schemas/                 # Validaciones Zod del recurso
│   │   ├── {recurso}.schema.ts
│   │   └── {subRecurso}.schema.ts
│   │
│   ├── types/                   # TypeScript types del recurso
│   │   ├── {recurso}.types.ts
│   │   └── {subRecurso}.types.ts
│   │
│   └── mappers/                 # Transformaciones (si necesario)
│       └── {recurso}.mapper.ts

├── shared/                      # ⭐ COMPARTIDO ENTRE RECURSOS
│   ├── components/              # Componentes reutilizables del módulo
│   ├── types/                   # Types compartidos
│   └── utils/                   # Utilidades compartidas

└── views/
    └── {Module}Home.tsx         # Vista principal del módulo

Principios clave:

  • Colocation: Archivos relacionados viven juntos
  • Mirror Pattern: services/schemas/types reflejan estructura de components
  • Progressive Disclosure: Complejidad revelada gradualmente
  • Scope Rule: Cada archivo en el scope más estrecho donde se reutiliza

Organización por Recursos

Principio Fundamental

Cada recurso de negocio tiene su "stack completo" de archivos relacionados agrupados en una carpeta dedicada. Esto sigue el principio de Colocation de React 19: mantener archivos relacionados cerca unos de otros.

Ejemplo: Recurso "Cliente" (Estructura Completa)

Cliente/                          # ⭐ Carpeta raíz del recurso
├── components/                   # UI del recurso
│   ├── ClienteForm/
│   │   ├── index.tsx            # Componente principal
│   │   ├── DatosBasicos.tsx     # Sección del form
│   │   ├── DatosContacto.tsx    # Sección del form
│   │   └── utils.ts             # Helpers específicos
│   ├── ClienteTable/
│   │   └── index.tsx
│   └── ClienteModals/
│       ├── AddModal.tsx
│       ├── EditModal.tsx
│       └── DeleteConfirmModal.tsx

├── views/                        # Vistas del recurso
│   ├── ClienteView.tsx
│   └── ClienteFormView.tsx

├── hooks/                        # Hooks del recurso
│   ├── useCliente.ts            # Query/Mutations
│   └── useClienteFormLogic.ts   # Lógica negocio

├── services/                     # API del recurso
│   └── cliente.service.ts

├── schemas/                      # Validaciones del recurso
│   └── cliente.schema.ts

├── types/                        # Types del recurso
│   └── cliente.types.ts

└── mappers/                      # Transformaciones (opcional)
    └── cliente.mapper.ts

Ventajas de esta estructura:

  • Colocation: Todo lo relacionado a Cliente está junto
  • Escalabilidad: Agregar recursos no contamina la raíz
  • Imports cortos: Menos ../../ en imports internos
  • Claridad: Jerarquía refleja dominio del negocio
  • Mantenibilidad: Cambios localizados en una carpeta

Stack Técnico por Recurso

CapaArchivoResponsabilidadObligatorio
Types{recurso}.types.tsDefiniciones TypeScript✅ Sí
Schemas{recurso}.schema.tsValidación Zod✅ Sí
Services{recurso}.service.tsAPI calls + lógica negocio✅ Sí
Mappers{recurso}.mapper.tsTransformación datos⚠️ Si necesario
Hooksuse{Recurso}.tsState management React✅ Sí
Hooksuse{Recurso}Crud.tsOperaciones CRUD⚠️ Si CRUD completo
Components{Recurso}Form.tsxFormulario principal⚠️ Si tiene form
Components{Recurso}Table.tsxTabla principal⚠️ Si tiene listado
ComponentsModales variosModales específicos⚠️ Según necesidad
Views{Recurso}View.tsxPágina listado✅ Sí
Views{Recurso}FormView.tsxPágina formulario⚠️ Si editable

Detalles de Cada Directorio

1. Archivo Principal ({Module}App.tsx)

Propósito: Punto de entrada de la aplicación React completa.

Responsabilidades:

  • Configurar providers (ConfigProvider, QueryClient)
  • Configurar routing (HashRouter + React Router)
  • Renderizar Layout con sidebar
  • Montar aplicación en DOM

Estructura típica:

typescript
import { StrictMode } from 'react';
import { HashRouter } from 'react-router-dom';
import Layout from '../core/components/layout/Layout.js';
import AppRouter from '../core/routing/AppRouter.js';
import { ConfigProvider } from '../core/context/index.js';
import type { AppConfiguration } from '../core/types/config.types.js';
import { moduloRoutes } from './config/routes.js';
import { moduloSidebarConfig } from './config/sidebar.js';
import './config/routeHierarchy.js';

interface ModuloAppProps {
    configuration: AppConfiguration;
    appVersion?: string;
}

export default function ModuloApp({ configuration, appVersion }: ModuloAppProps) {
    return (
        <StrictMode>
            <ConfigProvider config={configuration}>
                <HashRouter>
                    <Layout
                        sidebarConfig={{
                            moduleTitle: 'Mod. Nombre',
                            moduleLoc: '?loc=mxxx',
                            menuItems: moduloSidebarConfig,
                        }}
                        version={appVersion}
                    >
                        <AppRouter routes={moduloRoutes} />
                    </Layout>
                </HashRouter>
            </ConfigProvider>
        </StrictMode>
    );
}

2. Directorio config/

Propósito: Configuración centralizada del módulo.

queryKeys.ts

Responsabilidad: Definir claves TanStack Query para cache management.

Patrón:

typescript
export const moduloQueryKeys = {
    all: ['modulo'] as const,

    recursoA: {
        all: ['modulo', 'recursoA'] as const,
        list: (filters?: QueryOptions) =>
            [...moduloQueryKeys.recursoA.all, 'list', filters] as const,
        detail: (id: number) =>
            [...moduloQueryKeys.recursoA.all, 'detail', id] as const,
    },

    recursoB: {
        all: ['modulo', 'recursoB'] as const,
        list: (filters?: QueryOptions) =>
            [...moduloQueryKeys.recursoB.all, 'list', filters] as const,
    },
};

routes.tsx

Responsabilidad: Definir rutas React Router del módulo.

Patrón:

typescript
import type { RouteConfig } from '../../core/types/routing.types.js';
import ModuloHome from '../views/ModuloHome.js';
import RecursoView from '../views/RecursoView.js';
import RecursoFormView from '../views/RecursoFormView.js';

export const moduloRoutes: RouteConfig[] = [
    {
        path: '/',
        element: <ModuloHome />,
        breadcrumb: 'Inicio'
    },
    {
        path: '/bases/recursos',
        element: <RecursoView />,
        breadcrumb: 'Recursos'
    },
    {
        path: '/bases/recursos/nuevo',
        element: <RecursoFormView />,
        breadcrumb: 'Nuevo Recurso'
    },
    {
        path: '/bases/recursos/:id/editar',
        element: <RecursoFormView />,
        breadcrumb: 'Editar Recurso'
    },
];

Responsabilidad: Configurar menú lateral del módulo.

Patrón:

typescript
import { createStandardSidebar, createMenuItem } from '../../core/config/standardSidebarSections.js';
import type { MenuItem } from '../../core/types/layout.types.js';

export const moduloSidebarConfig: MenuItem[] = createStandardSidebar({
    moduleHome: '/',

    bases: [
        createMenuItem('recursos', 'Recursos', '/bases/recursos', {
            permission: 'MODULO_BASES_RECURSOS',
        }),
        createMenuItem('categorias', 'Categorías', '/bases/categorias', {
            permission: 'MODULO_BASES_CATEGORIAS',
            configKey: 'categoria', // Si requiere configuración sucursal
        }),
    ],

    movimientos: [
        createMenuItem('operacion', 'Operación', '/movimientos/operacion', {
            permission: 'MODULO_MOV_OPERACION',
        }),
    ],

    informes: [
        createMenuItem('reporte', 'Reporte', '/informes/reporte', {
            permission: 'MODULO_INF_REPORTE',
        }),
    ],

    utilidades: [
        createMenuItem('herramienta', 'Herramienta', '/utilidades/herramienta', {
            permission: 'MODULO_UTILS_HERRAMIENTA',
        }),
    ],
});

routeHierarchy.ts

Responsabilidad: Registrar jerarquía navegación para breadcrumbs inteligentes.

Patrón:

typescript
import { registerRouteHierarchy } from '../../core/routing/routeHierarchy.js';

registerRouteHierarchy({
    '/bases/recursos': '/',
    '/bases/recursos/nuevo': '/bases/recursos',
    '/bases/recursos/:id/editar': '/bases/recursos',
});

3. Directorio components/

Propósito: Componentes UI reutilizables del módulo.

Convenciones de nomenclatura:

  • Forms: {Recurso}Form.tsx
  • Tables: {Recurso}Table.tsx
  • Modales: {Action}{Recurso}Modal.tsx (Add, Edit, Delete, Confirm, etc.)
  • Secciones: {Recurso}Form{Seccion}.tsx
  • Toggles/Inputs: {Propiedad}Toggle.tsx, {Campo}Input.tsx

Organización recomendada: Dos enfoques válidos según complejidad.

Opción A: Flat Structure (módulos simples, <15 componentes)

components/
├── {Recurso}Form.tsx
├── {Recurso}Table.tsx
├── Add{Recurso}Modal.tsx
├── Edit{Recurso}Modal.tsx
└── Delete{Recurso}Modal.tsx

✅ Ventajas: Simplicidad, fácil navegación ❌ Desventajas: No escala bien con muchos recursos

Opción B: Grouped by Resource (módulos complejos, 15+ componentes)

components/
├── Miembro/                             # Recurso Miembro
│   ├── MiembroForm.tsx                  # Formulario maestro
│   ├── MiembroFormDatosBasicos.tsx      # Sección formulario
│   ├── MiembroFormDatosComerciales.tsx  # Sección formulario
│   ├── MiembroTable.tsx                 # Tabla principal
│   ├── AltaMiembroModal.tsx             # Modal alta
│   ├── BajaMiembroModal.tsx             # Modal baja
│   ├── AddMiembroDisciplinaModal.tsx    # Agregar disciplina
│   ├── EditMiembroDisciplinaModal.tsx   # Editar disciplina
│   └── MiembroDisciplinaListModal.tsx   # Lista disciplinas
├── Disciplina/                          # Recurso Disciplina
│   ├── DisciplinaForm.tsx
│   ├── AddDisciplinaModal.tsx
│   └── EditDisciplinaModal.tsx
├── Categoria/                           # Recurso Categoría
│   ├── CategoriaForm.tsx
│   ├── AssignCategoriaModal.tsx
│   └── DeleteCategoriaModal.tsx
├── GrupoFamiliar/                       # Recurso Grupo Familiar
│   ├── CreateGrupoFamiliarModal.tsx
│   ├── GrupoFamiliarListModal.tsx
│   └── AddMiembroGrupoModal.tsx
└── shared/                              # Componentes compartidos módulo
    ├── DefectoToggle.tsx
    ├── FichaMedicaToggle.tsx
    └── PrincipalToggle.tsx

✅ Ventajas:

  • Escalabilidad con muchos recursos
  • Cohesión alta (componentes del mismo recurso juntos)
  • Fácil ubicar componentes relacionados
  • Reducción "scroll fatigue" en IDE

❌ Desventajas:

  • Más carpetas, más profundidad
  • Imports más largos

Criterio de decisión:

  • Flat: < 15 componentes totales, 1-3 recursos principales
  • Grouped: 15+ componentes, 3+ recursos, crecimiento previsto

Principios comunes:

  • Un componente = Un archivo
  • Props explícitas con TypeScript
  • Componentes pequeños y especializados
  • Evitar lógica de negocio (delegar a hooks/services)
  • Carpeta shared/ para componentes cross-resource del módulo

4. Directorio views/

Propósito: Páginas principales del módulo (rutas completas).

Convenciones de nomenclatura:

  • Home: {Module}Home.tsx
  • Listado: {Recurso}View.tsx
  • Formulario: {Recurso}FormView.tsx
  • Detalle: {Recurso}DetailView.tsx
  • Operaciones: {Operacion}View.tsx

Responsabilidades de una View:

  • Layout de página (PageWrapper, PageHeader, PageContent)
  • Orquestar componentes
  • Gestionar modales principales
  • Manejar navegación
  • NO lógica de negocio (delegar a hooks)

Ejemplo típico:

typescript
import PageWrapper from '../../core/components/layout/PageWrapper.js';
import PageHeader from '../../core/components/layout/PageHeader.js';
import PageContent from '../../core/components/layout/PageContent.js';
import { RecursoTable } from '../components/RecursoTable.js';
import { AddRecursoModal } from '../components/AddRecursoModal.js';

export default function RecursoView() {
    const [isModalOpen, setIsModalOpen] = useState(false);

    return (
        <PageWrapper>
            <PageHeader
                title="Recursos"
                primaryAction={{
                    label: 'Nuevo Recurso',
                    onClick: () => setIsModalOpen(true),
                }}
            />
            <PageContent>
                <RecursoTable />
            </PageContent>
            <AddRecursoModal
                open={isModalOpen}
                onClose={() => setIsModalOpen(false)}
            />
        </PageWrapper>
    );
}

5. Directorio hooks/

Propósito: Custom hooks para state management y lógica React.

Convenciones de nomenclatura:

  • Query/Mutations básicos: use{Recurso}.ts
  • CRUD completo: use{Recurso}Crud.ts
  • Lógica formulario: use{Recurso}FormLogic.ts
  • Gestión modales: use{Recurso}Modals.ts
  • Tablas SSR: use{Recurso}TableSSR.ts

Patrón use{Recurso}.ts:

typescript
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { RecursoService } from '../services/recurso.service.js';
import { moduloQueryKeys } from '../config/queryKeys.js';
import { getProfileConfig } from '../../core/config/cacheProfiles.js';

export function useRecursoList(options = {}) {
    const cacheConfig = getProfileConfig('WARM');

    return useQuery({
        queryKey: moduloQueryKeys.recurso.list(options),
        queryFn: () => RecursoService.getAll(options),
        staleTime: cacheConfig.staleTime,
        gcTime: cacheConfig.gcTime,
    });
}

export function useRecursoById(id: number) {
    const cacheConfig = getProfileConfig('WARM');

    return useQuery({
        queryKey: moduloQueryKeys.recurso.detail(id),
        queryFn: () => RecursoService.getById(id),
        staleTime: cacheConfig.staleTime,
    });
}

export function useCreateRecurso() {
    const queryClient = useQueryClient();

    return useMutation({
        mutationFn: RecursoService.create,
        onSuccess: () => {
            queryClient.invalidateQueries({
                queryKey: moduloQueryKeys.recurso.all
            });
        },
    });
}

export function useUpdateRecurso() {
    const queryClient = useQueryClient();

    return useMutation({
        mutationFn: ({ id, data }) => RecursoService.update(id, data),
        onSuccess: (_, variables) => {
            queryClient.invalidateQueries({
                queryKey: moduloQueryKeys.recurso.detail(variables.id)
            });
            queryClient.invalidateQueries({
                queryKey: moduloQueryKeys.recurso.all
            });
        },
    });
}

export function useDeleteRecurso() {
    const queryClient = useQueryClient();

    return useMutation({
        mutationFn: RecursoService.delete,
        onSuccess: () => {
            queryClient.invalidateQueries({
                queryKey: moduloQueryKeys.recurso.all
            });
        },
    });
}

6. Directorio services/

Propósito: Lógica de negocio + llamadas API.

Convenciones de nomenclatura: {recurso}.service.ts

Responsabilidades:

  • Llamadas HTTP con Axios configurado
  • Transformaciones datos (usando mappers si existen)
  • Validaciones defensivas
  • Manejo errores API
  • NO state management React

Patrón estándar:

typescript
import api from '../../api/api.js';
import type { ApiResponse, QueryOptions, PaginatedResponse } from '../../api/api.types.js';

const basePath = 'mod-modulo/';

export interface RecursoQueryOptions extends QueryOptions {
    categoriaId?: number;
    status?: 'active' | 'inactive';
}

export const RecursoService = {
    /**
     * Obtener listado con paginación y filtros
     */
    getAll: async (options: RecursoQueryOptions = {}) => {
        const result = await api.get<ApiResponse<PaginatedResponse<Recurso>>>(
            `${basePath}recursos`,
            { params: options }
        );

        if (!result.data.data?.data || !Array.isArray(result.data.data.data)) {
            throw new Error('Invalid API response');
        }

        return {
            data: result.data.data.data,
            meta: result.data.data.meta,
        };
    },

    /**
     * Obtener por ID
     */
    getById: async (id: number) => {
        const result = await api.get<ApiResponse<Recurso>>(
            `${basePath}recursos/${id}`
        );
        return result.data.data;
    },

    /**
     * Crear nuevo
     */
    create: async (data: RecursoCreateData) => {
        const result = await api.post<ApiResponse<Recurso>>(
            `${basePath}recursos`,
            data
        );
        return result.data.data;
    },

    /**
     * Actualizar existente
     */
    update: async (id: number, data: RecursoUpdateData) => {
        const result = await api.put<ApiResponse<Recurso>>(
            `${basePath}recursos/${id}`,
            data
        );
        return result.data.data;
    },

    /**
     * Eliminar
     */
    delete: async (id: number) => {
        const result = await api.delete<ApiResponse<void>>(
            `${basePath}recursos/${id}`
        );
        return result.data;
    },
};

7. Directorio schemas/

Propósito: Validación Zod de formularios y datos.

Convenciones de nomenclatura: {recurso}.schema.ts

Patrón estándar:

typescript
import { z } from 'zod';

/**
 * Schema para crear recurso
 */
export const recursoCreateSchema = z.object({
    nombre: z.string()
        .min(1, 'El nombre es requerido')
        .max(100, 'Máximo 100 caracteres'),

    descripcion: z.string()
        .max(500, 'Máximo 500 caracteres')
        .optional(),

    categoriaId: z.number({
        required_error: 'La categoría es requerida',
        invalid_type_error: 'Categoría inválida',
    }).positive(),

    activo: z.boolean().default(true),

    email: z.string()
        .email('Email inválido')
        .optional(),

    precio: z.number()
        .min(0, 'El precio no puede ser negativo')
        .optional(),
});

/**
 * Schema para actualizar recurso (todos campos opcionales)
 */
export const recursoUpdateSchema = recursoCreateSchema.partial();

/**
 * TypeScript types inferidos
 */
export type RecursoCreateSchemaType = z.infer<typeof recursoCreateSchema>;
export type RecursoUpdateSchemaType = z.infer<typeof recursoUpdateSchema>;

8. Directorio types/

Propósito: Definiciones TypeScript del dominio.

Convenciones de nomenclatura: {recurso}.types.ts

Patrón estándar:

typescript
/**
 * Entidad principal
 */
export interface Recurso {
    id: number;
    nombre: string;
    descripcion?: string | null;
    categoriaId: number;
    activo: boolean;
    createdAt: string;
    updatedAt: string;

    // Relaciones (con include)
    categoria?: Categoria | null;
    items?: Item[] | null;
}

/**
 * Tipos auxiliares
 */
export interface RecursoCreateData {
    nombre: string;
    descripcion?: string;
    categoriaId: number;
    activo?: boolean;
}

export interface RecursoUpdateData extends Partial<RecursoCreateData> {}

export interface RecursoFilters {
    categoriaId?: number;
    activo?: boolean;
    search?: string;
}

/**
 * Enums del dominio
 */
export enum RecursoStatus {
    ACTIVO = 'activo',
    INACTIVO = 'inactivo',
    PENDIENTE = 'pendiente',
}

/**
 * Type guards
 */
export function isRecursoActivo(recurso: Recurso): boolean {
    return recurso.activo === true;
}

9. Directorio mappers/ (Opcional)

Propósito: Transformar datos entre backend y frontend cuando estructuras difieren.

Cuándo usar:

  • Backend y frontend tienen estructuras diferentes
  • Necesitas transformar tipos de datos (fechas, enums, nested objects)
  • Campos con nombres diferentes
  • Aplanar/anidar estructuras

Cuándo NO usar:

  • Estructuras idénticas backend ↔ frontend
  • Transformaciones simples que puede hacer el service

Convenciones de nomenclatura: {recurso}.mapper.ts

Patrón estándar:

typescript
/**
 * Backend → Frontend
 */
export function mapRecursoToFrontend(backend: RecursoBackend): Recurso {
    return {
        id: backend.id,
        nombre: backend.nombre,
        descripcion: backend.descripcion ?? null,
        activo: backend.estado === 'A',
        categoriaId: backend.categoria_id,
        createdAt: backend.fecha_creacion,

        // Transformaciones complejas
        categoria: backend.categoria
            ? mapCategoriaToFrontend(backend.categoria)
            : null,
    };
}

/**
 * Frontend → Backend
 */
export function mapRecursoToBackend(form: RecursoCreateData): RecursoBackendCreate {
    return {
        nombre: form.nombre,
        descripcion: form.descripcion ?? null,
        categoria_id: form.categoriaId,
        estado: form.activo ? 'A' : 'I',
    };
}

Flujos de Datos

Lectura (Query)

View Component
  ↓ usa
Hook (useRecurso)
  ↓ llama
Service (RecursoService.getAll)
  ↓ API call
Backend
  ↓ respuesta
Service (transforma con mapper si existe)
  ↓ datos transformados
Hook (cachea con TanStack Query)
  ↓ estado
View Component (renderiza)

Escritura (Mutation)

View Component (submit form)
  ↓ usa
Hook (useCreateRecurso)
  ↓ valida
Zod Schema
  ↓ si válido, llama
Service (RecursoService.create)
  ↓ transforma (mapper)
Backend
  ↓ success
Hook mutation (invalidate cache)
  ↓ actualiza
View Component (re-renderiza con nuevos datos)
  ↓ muestra
Toast Notification

Decisiones Arquitectónicas

ADR-001: Separación Hooks vs Services

Decisión: Separar hooks (React) de services (lógica pura).

Contexto: Necesidad de reutilizar lógica fuera de React y facilitar testing.

Consecuencias:

✅ Positivas:

  • Services testeables sin React Testing Library
  • Lógica reutilizable en scripts/workers
  • Separación clara de responsabilidades
  • TanStack Query centralizado en hooks

❌ Negativas:

  • Más archivos por recurso
  • Duplicación aparente (hook llama service)

Alternativas rechazadas:

  • Todo en hooks: dificulta testing y reutilización
  • Todo en services: mezcla concerns React con lógica pura

ADR-002: Organización por Recursos vs por Tipo

Decisión: Organizar por capas técnicas (types/, services/, hooks/), no por recursos.

Contexto: Balance entre cohesión de recurso y separación de concerns.

Consecuencias:

✅ Positivas:

  • Separación clara de responsabilidades
  • Fácil ubicar archivos por tipo
  • Convenciones claras de nomenclatura
  • IDE autocomplete eficiente

❌ Negativas:

  • Stack de un recurso distribuido en múltiples carpetas
  • Más navegación entre directorios

Alternativas consideradas:

  • Por recursos: recursos/cliente/{types,services,hooks,components}
    • ✅ Cohesión alta
    • ❌ Dificulta encontrar "todos los services"
    • ❌ No escala con recursos pequeños

Decisión final: Capas técnicas, documentando claramente qué archivos pertenecen a cada recurso.

ADR-003: Mappers Opcionales

Decisión: Mappers solo cuando hay diferencias significativas backend ↔ frontend.

Contexto: Evitar over-engineering en casos simples.

Regla:

  • SÍ usar mappers: Estructuras diferentes, transformaciones complejas, múltiples transformaciones
  • NO usar mappers: Estructuras idénticas, transformaciones triviales

ADR-004: Configuración Centralizada

Decisión: Directorio config/ obligatorio con queryKeys, routes, sidebar.

Contexto: Facilitar cambios globales y convenciones consistentes.

Consecuencias:

  • ✅ Cambios de rutas centralizados
  • ✅ Cache keys tipados y consistentes
  • ✅ Sidebar declarativo
  • ❌ Indirección adicional

ADR-005: Agrupación por Recurso en Todas las Carpas (RECOMENDADO)

Decisión: Organizar TODAS las carpetas (components, hooks, services, schemas, types, views) por recurso de negocio, no por tipo técnico.

Contexto: Módulos complejos con múltiples recursos dificultan encontrar archivos relacionados cuando están dispersos por tipo técnico.

Estructura Anterior (Por Tipo Técnico):

mod-membresias/
├── components/
│   ├── MiembroForm.tsx
│   ├── CategoriaForm.tsx
│   └── DisciplinaForm.tsx
├── hooks/
│   ├── useMiembro.ts
│   ├── useCategoria.ts
│   └── useDisciplina.ts
├── services/
│   ├── miembro.service.ts
│   ├── categoria.service.ts
│   └── disciplina.service.ts
└── types/
    ├── miembro.types.ts
    ├── categoria.types.ts
    └── disciplina.types.ts

Estructura Nueva (Por Recurso - RECOMENDADA):

mod-membresias/
├── Miembro/
│   ├── components/
│   ├── hooks/
│   ├── services/
│   ├── schemas/
│   ├── types/
│   └── views/
├── Categoria/
│   ├── components/
│   ├── hooks/
│   ├── services/
│   ├── schemas/
│   └── types/
└── Disciplina/
    ├── components/
    ├── hooks/
    ├── services/
    ├── schemas/
    └── types/

Criterios de decisión:

CriterioPor Tipo TécnicoPor Recurso
RecursosCualquier cantidad3+ recursos
CohesiónBajaAlta ⭐
ColocationNoSí ⭐
EscalabilidadMediaAlta ⭐
NavegaciónPor tipoPor dominio ⭐
Imports internosLargos (../../)Cortos (../) ⭐

Consecuencias:

✅ Positivas:

  • Colocation (React 19): Archivos relacionados viven juntos
  • Mental Model: Estructura refleja dominio de negocio
  • Refactoring: Cambios aislados en carpeta de recurso
  • Onboarding: Más fácil entender organización
  • Git: Menos conflictos (cada recurso es independiente)

❌ Negativas:

  • Más carpetas en la raíz (mitigado con buenos nombres)
  • Cambio de paradigma (requiere documentación)

Regla práctica: Si el módulo tiene 3+ recursos distintos, agrupar por recurso.

Ejemplo real: mod-membresias con 6 recursos (Miembro, Categoria, Disciplina, GrupoFamiliar, TipoRelacion, FacturacionLotes).

ADR-006: Uso de index.tsx en Componentes

Decisión: Permitir dos enfoques para components/: flat (< 15 componentes) o grouped by resource (15+ componentes).

Contexto: Módulos complejos con muchos recursos (ej: Membresías con 32+ componentes) dificultan navegación en estructura flat.

Opción A: Flat Structure

components/
├── RecursoAForm.tsx
├── RecursoBTable.tsx
└── AddRecursoAModal.tsx

Opción B: Grouped Structure

components/
├── RecursoA/
│   ├── RecursoAForm.tsx
│   └── AddRecursoAModal.tsx
├── RecursoB/
│   └── RecursoBTable.tsx
└── shared/
    └── CommonToggle.tsx

Criterios de decisión:

CriterioFlatGrouped
Componentes totales< 1515+
Recursos principales1-33+
CohesiónBajaAlta
NavegaciónSimpleRequiere carpetas
EscalabilidadLimitadaAlta

Consecuencias:

✅ Positivas (Grouped):

  • Alta cohesión (componentes relacionados juntos)
  • Fácil ubicar componentes por recurso
  • Reduce "scroll fatigue" en IDE
  • Facilita refactoring de recursos completos
  • Carpeta shared/ clara para cross-resource

❌ Negativas (Grouped):

  • Más profundidad directorios
  • Imports ligeramente más largos
  • Necesita disciplina en nomenclatura

Regla práctica: Si el módulo tiene 3+ recursos con 5+ componentes cada uno, usar grouped structure.

Ejemplo real: mod-membresias usa grouped con 6 recursos (Miembro, Disciplina, Categoria, GrupoFamiliar, TipoRelacion, FacturacionLotes).

Checklist de Implementación

Al crear un nuevo módulo:

  • [ ] Crear archivo principal {Module}App.tsx
  • [ ] Crear directorio config/ con 4 archivos obligatorios
  • [ ] Definir estructura inicial de rutas en routes.tsx
  • [ ] Configurar sidebar en sidebar.ts
  • [ ] Crear queryKeys.ts con estructura base
  • [ ] Registrar jerarquía en routeHierarchy.ts

Al agregar un nuevo recurso:

  • [ ] types/{recurso}.types.ts - Definir interfaces
  • [ ] schemas/{recurso}.schema.ts - Definir validaciones
  • [ ] services/{recurso}.service.ts - Implementar CRUD API
  • [ ] hooks/use{Recurso}.ts - Implementar queries/mutations
  • [ ] components/{Recurso}*.tsx - Crear componentes necesarios
  • [ ] views/{Recurso}View.tsx - Crear vista principal
  • [ ] Agregar ruta en config/routes.tsx
  • [ ] Agregar menú en config/sidebar.ts
  • [ ] mappers/{recurso}.mapper.ts - Solo si necesario

Ejemplo Completo: Módulo Productos

Opción A: Flat Structure (Módulo Simple)

ts/mod-productos/
├── ProductosApp.tsx

├── config/
│   ├── queryKeys.ts
│   ├── routeHierarchy.ts
│   ├── routes.tsx
│   └── sidebar.ts

├── components/
│   ├── ProductoForm.tsx
│   ├── ProductoTable.tsx
│   ├── AddProductoModal.tsx
│   ├── EditProductoModal.tsx
│   ├── DeleteProductoModal.tsx
│   ├── CategoriaForm.tsx
│   └── AddCategoriaModal.tsx

├── views/
│   ├── ProductosHome.tsx
│   ├── ProductoView.tsx
│   ├── ProductoFormView.tsx
│   └── CategoriaView.tsx

├── hooks/
│   ├── useProducto.ts
│   ├── useProductoCrud.ts
│   └── useCategoria.ts

├── services/
│   ├── producto.service.ts
│   └── categoria.service.ts

├── schemas/
│   ├── producto.schema.ts
│   └── categoria.schema.ts

└── types/
    ├── producto.types.ts
    ├── categoria.types.ts
    └── global.d.ts

Opción B: Grouped Structure (Módulo Complejo - Como Membresías)

ts/mod-membresias/
├── MembershipsApp.tsx

├── config/
│   ├── queryKeys.ts
│   ├── routeHierarchy.ts
│   ├── routes.tsx
│   └── sidebar.ts

├── components/                          # 32+ componentes organizados
│   ├── Miembro/                         # 16 componentes del recurso Miembro
│   │   ├── MiembroForm.tsx
│   │   ├── MiembroFormDatosBasicos.tsx
│   │   ├── MiembroFormDatosComerciales.tsx
│   │   ├── MiembroFormDatosMembresia.tsx
│   │   ├── MiembroFormAccionesAdicionales.tsx
│   │   ├── MiembroTable.tsx
│   │   ├── AltaMiembroModal.tsx
│   │   ├── BajaMiembroModal.tsx
│   │   ├── AddMiembroDisciplinaModal.tsx
│   │   ├── EditMiembroDisciplinaModal.tsx
│   │   ├── ConfirmDeleteMiembroDisciplinaModal.tsx
│   │   ├── MiembroDisciplinaListModal.tsx
│   │   ├── AddMiembroProductoModal.tsx
│   │   ├── EditMiembroProductoModal.tsx
│   │   ├── ConfirmDeleteMiembroProductoModal.tsx
│   │   └── MiembroProductoListModal.tsx
│   ├── Disciplina/
│   │   └── DisciplinaForm.tsx
│   ├── Categoria/
│   │   ├── CategoriaMembresiaForm.tsx
│   │   ├── DeleteCategoriaMembresiaModal.tsx
│   │   └── AssignCategoriaModal.tsx
│   ├── GrupoFamiliar/
│   │   ├── CreateGrupoFamiliarModal.tsx
│   │   ├── GrupoFamiliarListModal.tsx
│   │   ├── GrupoFamiliarResumenModal.tsx
│   │   ├── AddMiembroGrupoModal.tsx
│   │   ├── EditMiembroGrupoModal.tsx
│   │   └── ConfirmDeleteMiembroGrupoModal.tsx
│   ├── TipoRelacion/
│   │   └── TipoRelacionForm.tsx
│   ├── FacturacionLotes/
│   │   ├── FacturacionLotesForm.tsx
│   │   └── FacturacionLotesResultModal.tsx
│   └── shared/                          # Componentes compartidos módulo
│       ├── DefectoToggle.tsx
│       ├── FichaMedicaToggle.tsx
│       └── PrincipalToggle.tsx

├── views/
│   ├── MembershipsHome.tsx
│   ├── MiembroView.tsx
│   ├── MiembroFormView.tsx
│   ├── TipoRelacionView.tsx
│   ├── DisciplinaView.tsx
│   ├── CategoriaMembresiaView.tsx
│   ├── AsignacionCategoriasView.tsx
│   ├── AsignacionProductosView.tsx
│   ├── FacturacionLotesView.tsx
│   └── GenerarCuponView.tsx

├── hooks/                               # 14 hooks
│   ├── useMiembro.ts
│   ├── useMiembroFormLogic.ts
│   ├── useMiembroFormModals.ts
│   ├── useMiembroTableSSR.ts
│   ├── useDisciplina.ts
│   ├── useDisciplinaCrud.ts
│   ├── useCategoriaMembresia.ts
│   ├── useCategoriaMembresiacrud.ts
│   ├── useTipoRelacion.ts
│   ├── useTipoRelacionCrud.ts
│   ├── useGrupoFamiliar.ts
│   ├── useMiembroDisciplina.ts
│   └── useMiembroProducto.ts

├── services/                            # 8 services
│   ├── miembro.service.ts
│   ├── disciplina.service.ts
│   ├── categoriaMembresia.service.ts
│   ├── tipoRelacion.service.ts
│   ├── grupoFamiliar.service.ts
│   ├── miembroDisciplina.service.ts
│   ├── miembroProducto.service.ts
│   └── facturacionLotes.service.ts

├── schemas/                             # 10 schemas
│   ├── miembro.schema.ts
│   ├── disciplina.schema.ts
│   ├── categoriaMembresia.schema.ts
│   ├── tipoRelacion.schema.ts
│   ├── grupoFamiliar.schema.ts
│   ├── miembroDisciplina.schema.ts
│   ├── miembroProducto.schema.ts
│   ├── bulkMiembroProducto.schema.ts
│   ├── asignacionCategoria.schema.ts
│   ├── facturacionLotes.schema.ts
│   └── selectionMode.schema.ts

├── types/                               # 11 types
│   ├── miembro.types.ts
│   ├── disciplina.types.ts
│   ├── categoriaMembresia.types.ts
│   ├── tipoRelacion.types.ts
│   ├── grupoFamiliar.types.ts
│   ├── miembroDisciplina.types.ts
│   ├── miembroProducto.types.ts
│   ├── miembroProductoBulk.types.ts
│   ├── bulkAssignment.types.ts
│   ├── facturacionLotes.types.ts
│   ├── selectionMode.types.ts
│   └── global.d.ts

└── mappers/
    └── miembro.mapper.ts

Comparación de enfoques:

CriterioFlat (Productos)Grouped (Membresías)
Componentes732+
Recursos26+
NavegaciónMás simpleRequiere carpetas
EscalabilidadLimitadaAlta
MantenibilidadAlta (simple)Alta (organizada)
Cuándo usar< 15 componentes15+ componentes

Referencias

  • Implementación real: Ver ts/mod-membresias/ como ejemplo completo
  • Core components: ts/core/ para componentes compartidos
  • API integration: ts/api/ para configuración Axios
  • Testing: tests/ para estrategias de testing

Autor: Sistema Bautista - Arquitectura Frontend Revisión: 2026-01-29 Estado: Estándar vigente