Skip to content

Kevinparra535/clean-architecture-stack

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

15 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Clean Architecture Stack

Una arquitectura opinada y consistente para construir apps React, React Native y librerías npm mantenibles, basada en MVVM + MobX + Inversify + Clean Architecture.

License: MIT Status

Clean Architecture Stack — capas concéntricas


¿Qué es esto?

Este repositorio es la documentación de una arquitectura que he venido refinando en proyectos React y React Native. No es un framework, no es un paquete npm, no es un boilerplate. Es un conjunto de reglas, convenciones y plantillas que cualquier desarrollador (o LLM, ver más abajo) puede leer y aplicar para escribir código mantenible, testeable y consistente.

Encontrarás aquí:

  • Filosofía (docs/00-philosophy.md) — los principios rectores que motivan cada decisión.
  • Skills (skills/) — archivos .md con instrucciones precisas, diseñados para que tanto humanos como asistentes de IA (Claude, Cursor, Copilot) los consuman y generen código que cumpla las reglas.
  • ADRs (docs/02-decision-records/) — registros de decisiones de arquitectura que explican el "por qué" detrás de cada elección.
  • Ejemplos (examples/) — snippets canónicos de cada pieza (ViewModel, UseCase, Entity, Repository, DI bindings).

¿Por qué otra arquitectura?

El ecosistema React actual está dominado por hooks, Zustand/Jotai, server components y patrones funcionales. Son excelentes para muchos casos. Esta arquitectura no compite con ellos en su terreno: compite cuando:

  • El dominio del negocio es rico (no eres un wrapper de un CRUD).
  • El equipo es mediano o grande (3+ desarrolladores).
  • El código va a vivir años y va a ser tocado por gente que no estuvo en su diseño original.
  • Quieres que agregar una feature siempre se sienta igual, sin importar quién la escriba.

En esos contextos, la disciplina de capas + DI + ViewModels rinde mucho más que un montón de hooks compartidos. Si tu app es un dashboard de 5 pantallas con 2 endpoints, esto es overkill — usa Zustand y sigue tu camino.

Cuándo SÍ usar esto / Cuándo NO

Úsalo cuando… Evítalo cuando…
Tu app tiene reglas de negocio no triviales Es un sitio mayormente estático o de marketing
Vas a tener múltiples fuentes de datos (REST, GraphQL, FB) Tienes un solo endpoint y nada más
El equipo crece y necesitas onboarding predecible Estás solo y vas a quedarte solo
Quieres testear lógica sin levantar la UI El código es desechable o un prototipo
Necesitas reemplazar el backend sin tocar la UI El backend es estable y nunca va a cambiar
Vas a tener varios productos compartiendo dominio Es una app one-shot

Más detalle en docs/04-when-not-to-use.md.

Tour rápido (5 min)

Vamos a ver el flujo completo de "agregar un cliente", de la pantalla hasta la API.

sequenceDiagram
    participant S as ClientsScreen
    participant ViewModel as ClientsViewModel
    participant UC as GetAllClientUseCase
    participant R as ClientRepository
    participant API as REST API

    S->>ViewModel: viewModel.loadAll()
    ViewModel->>ViewModel: updateLoadingState(true)
    ViewModel->>UC: uc.run()
    UC->>R: repo.getAll()
    R->>API: GET /clients
    API-->>R: ClientModel[]
    R->>R: models.map(m => m.toDomain())
    R-->>UC: Client[]
    UC-->>ViewModel: Client[]
    ViewModel->>ViewModel: runInAction(() => isItemsResponse = data)
    ViewModel->>ViewModel: updateLoadingState(false)
    ViewModel-->>S: re-render (observer)
Loading

1. La pantalla solo bindea inputs y llama a la ViewModel

// src/ui/screens/Clients/ClientsScreen.tsx
const ClientsScreen = observer(() => {
  const viewModel = useViewModel<ClientsViewModel>(TYPES.ClientsViewModel);

  useEffect(() => { viewModel.loadAll(); }, [viewModel]);

  return (
    <SafeAreaView>
      {viewModel.isClientsLoading && <Spinner />}
      {viewModel.isClientsError && <Text>{viewModel.isClientsError}</Text>}
      <FlatList data={viewModel.isClientsResponse} ... />
      <PrimaryButton label="Agregar" onPress={() => viewModel.create(formValues)} />
    </SafeAreaView>
  );
});

2. La ViewModel orquesta estado y delega en UseCases

// src/ui/screens/Clients/ClientsViewModel.ts
@injectable()
export class ClientsViewModel {
  isClientsLoading = false;
  isClientsError: string | null = null;
  isClientsResponse: Client[] | null = null;

  constructor(
    @inject(TYPES.GetAllClientUseCase) private getAll: GetAllClientUseCase,
    @inject(TYPES.CreateClientUseCase) private createUC: CreateClientUseCase,
  ) {
    makeAutoObservable(this);
  }

  async loadAll() {
    this.updateLoadingState(true, null, 'items');
    try {
      const response = await this.getAll.run();
      runInAction(() => { this.isClientsResponse = response; });
      this.updateLoadingState(false, null, 'items');
    } catch (e) { this.handleError(e, 'items'); }
  }

  // ... updateLoadingState, handleError, create, etc.
}

3. El UseCase ejecuta UNA acción de negocio

// src/domain/useCases/GetAllClientUseCase/index.ts
@injectable()
export class GetAllClientUseCase implements UseCase<void, Client[]> {
  constructor(
    @inject(TYPES.ClientRepository) private repo: ClientRepository,
  ) {}

  async run(): Promise<Client[]> {
    return this.repo.getAll();
  }
}

4. La interfaz del repositorio vive en domain/

// src/domain/repositories/ClientRepository.ts
export interface ClientRepository {
  getAll(): Promise<Client[]>;
  create(client: Client): Promise<Client>;
  // ...
}

5. La implementación vive en data/ y mapea modelos a entidades

// src/data/repositories/ClientRepositoryImpl.ts
@injectable()
export class ClientRepositoryImpl implements ClientRepository {
  constructor(
    @inject(TYPES.ClientService) private service: ClientService,
  ) {}

  async getAll(): Promise<Client[]> {
    const models = await this.service.fetchAll();
    return models.map(m => m.toDomain());
  }
}

Eso es todo. Cualquier feature nueva sigue ese mismo flujo, escrito por cualquier persona del equipo, queda igual. Esa es la promesa.

Ver el flujo completo de archivos en examples/.

Las reglas no negociables

flowchart LR
    UI["🖥️ UI<br/>Screens + Components"]
    ViewModel["🧠 ViewModel<br/>(MobX + Inversify)"]
    UC["⚙️ UseCases<br/>(1 acción = 1 UC)"]
    REPO["📜 Repository<br/>Interface (domain)"]
    IMPL["🔌 Repository<br/>Impl (data)"]
    SVC["🌐 Service<br/>HTTP / Firebase"]

    UI --> ViewModel
    ViewModel --> UC
    UC --> REPO
    IMPL -.implements.-> REPO
    IMPL --> SVC

    style UI fill:#1A2F5E,stroke:#2D7EF8,color:#fff
    style ViewModel fill:#1A2F5E,stroke:#2D7EF8,color:#fff
    style UC fill:#0A1628,stroke:#2D7EF8,color:#fff
    style REPO fill:#0A1628,stroke:#2D7EF8,color:#fff
    style IMPL fill:#0A1628,stroke:#9B59B6,color:#fff
    style SVC fill:#0A1628,stroke:#9B59B6,color:#fff
Loading
  1. UI depende solo del ViewModel. No importa data/, no importa Firebase, no importa axios.
  2. ViewModel depende solo de UseCases. No conoce repositorios.
  3. UseCases dependen solo de contratos del dominio (interfaces).
  4. domain/ no importa nada de framework ni infraestructura. Es portable.
  5. Cada acción de negocio = un UseCase. Uno solo, en su propia carpeta.
  6. Los modelos de transporte (DTOs) nunca llegan a la UI. Siempre mapeados a entidades del dominio.
  7. Las ViewModels son UI-agnósticas. No tienen Alert, ni navigate, ni hooks, ni window.
  8. Cada Screen vive en su propia carpeta con su ViewModel. src/ui/screens/<Feature>/<Feature>Screen.tsx y <Feature>ViewModel.ts, sin excepciones.
  9. La Screen solo compone UI. Sin subcomponentes internos, sin transformación de datos, sin branching de negocio; los componentes privados van en components/.
  10. Las constantes no viven en la Screen. Tokens visuales en src/ui/styles; configuración y opciones reusables en src/config.

Stack y decisiones

Pieza Elección Alternativa rechazada ADR
Estado MobX (makeAutoObservable) Zustand, Redux Toolkit 001
Inyección Inversify (@injectable + TYPES) React Context, factories 002
Entidades Clases con [key: string]: any Interfaces puras 003
Granularidad ViewModel→UC 1 acción = 1 UseCase Service con N métodos 004
Patrón ViewModel ICalls + updateLoadingState useState por flag 005
Logging Logger con scope (no inyectado) console.*, ILogger por DI 006
Streams realtime SubscriptionUseCase run(): Promise<Unsubscribe> 007
Estado compartido Stores singleton (MobX) React Context, RootStore god-object 008
Transporte (data) Capa Manager separada de Service Service habla Axios/Firebase directo 009
Formato de skills Frontmatter + secciones XML Markdown libre 010
Boundary de Screens Carpeta propia + Screen visual Screens con lógica/subcomponentes 011

Cómo aplicarlo a tu proyecto

  1. Lee este README completo y la filosofía detrás de las decisiones.
  2. Revisa el Tour rápido y los ejemplos canónicos.
  3. Consulta la skill que aplica a tu plataforma:
  4. Para cada feature nueva, sigue el PR Checklist.

Más detalle en docs/01-getting-started.md.

Las skills (uso con LLMs)

Las skills en skills/ no son documentación pasiva. Están escritas como instrucciones ejecutables para asistentes de IA: si pegas el contenido de feature-scaffold-rn.md en Claude Projects, Cursor Rules, o un system prompt, el LLM va a generar features que cumplen estas reglas sin que tengas que repetirlas en cada prompt.

Todas siguen un formato canónico (frontmatter + secciones XML <purpose>/<when_to_use>/<rules>/<examples>/<output_format>/<see_also>) basado en las prácticas de prompt y context engineering de Anthropic. El estándar de formato y la convención para instalarlas y mantenerlas en cada proyecto (p.ej. .claude/skills/<name>/SKILL.md) viven en skill-authoring. Este repo es la fuente de verdad; los proyectos consumen copias y se resincronizan desde aquí.

Skill Propósito
clean-architecture-rn-expo-mvvm Reglas generales de arquitectura (RN Expo)
feature-scaffold-rn Scaffold completo de una feature vertical
unit-testing-clean-architecture Tests unitarios (obligatorios) — contrato completo
realtime-and-global-state-rn Streams realtime, stores globales y offline/sync
design-system-rn Tokens y componentes del design system
pr-checklist-clean-architecture Checklist para revisar PRs
skill-authoring Formato canónico de las skills + cómo mantenerlas en proyectos

FAQ rápido

¿Por qué MobX en 2026? Porque makeAutoObservable + clases es el match perfecto para MVVM y la ViewModel-as-class. Zustand es excelente, pero te empuja a un estilo funcional/hooks que choca con la disciplina de capas que buscamos. Detalle en ADR 001.

¿Inversify no es exagerado para React? Para una app pequeña, sí. Para apps con 20+ pantallas, decenas de UseCases y múltiples adaptadores de datos, Inversify se paga solo. Detalle en ADR 002.

¿Por qué [key: string]: any en entidades? Es una concesión consciente: privilegia velocidad de iteración con backends inestables sobre tipado exhaustivo. Detalle en ADR 003.

¿"1 acción = 1 UseCase" no genera explosión de archivos? En apps puramente CRUD, sí. En apps con dominio rico, esa explosión es exactamente lo que da claridad. Detalle en ADR 004.

Más en docs/03-faq.md.

Roadmap

  • Fase 1 — Skills RN + docs base + ADRs principales (este release)
  • 🚧 Fase 2 — Skills React web + monorepo/npm package
  • 🔭 Fase 3 — Skills Python (FastAPI + hexagonal)
  • 🔭 Fase 4 — CLI generador de features (npx cas-cli new-feature Clients)

Detalle en ROADMAP.md. Historial de cambios en CHANGELOG.md.

Contribuir

Las skills evolucionan con el uso real. Si encuentras un caso que no cubren, una regla que choca con tu contexto, o un patrón mejor: abre un issue o PR. Ver CONTRIBUTING.md.

Licencia

MIT — ver LICENSE.


Escrito por @Kevinparra535. Si esto te ayudó, deja una estrella ⭐ en el repo.

About

Opinionated Clean Architecture for React, React Native, and npm packages. MVVM + MobX + Inversify, with AI-ready skills for consistent feature scaffolding.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors