{"id":49820248,"url":"https://github.com/kevinparra535/clean-architecture-stack","last_synced_at":"2026-05-13T10:06:07.125Z","repository":{"id":354497628,"uuid":"1223891016","full_name":"Kevinparra535/clean-architecture-stack","owner":"Kevinparra535","description":"Opinionated Clean Architecture for React, React Native, and npm packages. MVVM + MobX + Inversify, with AI-ready skills for consistent feature scaffolding.","archived":false,"fork":false,"pushed_at":"2026-04-28T20:39:21.000Z","size":1421,"stargazers_count":1,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-04-28T21:21:23.723Z","etag":null,"topics":["ai-tooling","arquitechture","claude-skills","clean-architecture","dependency-injection","expo","inversifyjs","mobx","monorepo","mvvm-architecture","react","react-native","solid","typescript"],"latest_commit_sha":null,"homepage":"","language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Kevinparra535.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":"ROADMAP.md","authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-28T18:54:40.000Z","updated_at":"2026-04-28T20:39:26.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/Kevinparra535/clean-architecture-stack","commit_stats":null,"previous_names":["kevinparra535/clean-architecture-stack"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/Kevinparra535/clean-architecture-stack","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kevinparra535%2Fclean-architecture-stack","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kevinparra535%2Fclean-architecture-stack/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kevinparra535%2Fclean-architecture-stack/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kevinparra535%2Fclean-architecture-stack/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Kevinparra535","download_url":"https://codeload.github.com/Kevinparra535/clean-architecture-stack/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kevinparra535%2Fclean-architecture-stack/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32977344,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-13T06:31:55.726Z","status":"ssl_error","status_checked_at":"2026-05-13T06:31:51.336Z","response_time":115,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai-tooling","arquitechture","claude-skills","clean-architecture","dependency-injection","expo","inversifyjs","mobx","monorepo","mvvm-architecture","react","react-native","solid","typescript"],"created_at":"2026-05-13T10:06:06.005Z","updated_at":"2026-05-13T10:06:07.113Z","avatar_url":"https://github.com/Kevinparra535.png","language":null,"funding_links":[],"categories":[],"sub_categories":[],"readme":"# Clean Architecture Stack\n\n\u003e Una arquitectura opinada y consistente para construir apps React, React Native y librerías npm mantenibles, basada en **MVVM + MobX + Inversify + Clean Architecture**.\n\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)\n[![Status](https://img.shields.io/badge/status-fase%201-blue)](./ROADMAP.md)\n\n![Clean Architecture Stack — capas concéntricas](./docs/assets/hero.webp)\n\n---\n\n## ¿Qué es esto?\n\nEste 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.\n\nEncontrarás aquí:\n\n- **Filosofía** ([`docs/00-philosophy.md`](./docs/00-philosophy.md)) — los principios rectores que motivan cada decisión.\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.\n- **ADRs** (`docs/02-decision-records/`) — registros de decisiones de arquitectura que explican el \"por qué\" detrás de cada elección.\n- **Ejemplos** (`examples/`) — snippets canónicos de cada pieza (ViewModel, UseCase, Entity, Repository, DI bindings).\n\n## ¿Por qué otra arquitectura?\n\nEl 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:\n\n- El dominio del negocio es **rico** (no eres un wrapper de un CRUD).\n- El equipo es **mediano o grande** (3+ desarrolladores).\n- El código va a **vivir años** y va a ser tocado por gente que no estuvo en su diseño original.\n- Quieres que **agregar una feature siempre se sienta igual**, sin importar quién la escriba.\n\nEn 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.\n\n## Cuándo SÍ usar esto / Cuándo NO\n\n| Úsalo cuando…                                              | Evítalo cuando…                                         |\n| ---------------------------------------------------------- | ------------------------------------------------------- |\n| Tu app tiene reglas de negocio no triviales                | Es un sitio mayormente estático o de marketing          |\n| Vas a tener múltiples fuentes de datos (REST, GraphQL, FB) | Tienes un solo endpoint y nada más                      |\n| El equipo crece y necesitas onboarding predecible          | Estás solo y vas a quedarte solo                        |\n| Quieres testear lógica sin levantar la UI                  | El código es desechable o un prototipo                  |\n| Necesitas reemplazar el backend sin tocar la UI            | El backend es estable y nunca va a cambiar              |\n| Vas a tener varios productos compartiendo dominio          | Es una app one-shot                                     |\n\nMás detalle en [`docs/04-when-not-to-use.md`](./docs/04-when-not-to-use.md).\n\n## Tour rápido (5 min)\n\nVamos a ver el flujo completo de **\"agregar un cliente\"**, de la pantalla hasta la API.\n\n```mermaid\nsequenceDiagram\n    participant S as ClientsScreen\n    participant VM as ClientsViewModel\n    participant UC as GetAllClientUseCase\n    participant R as ClientRepository\n    participant API as REST API\n\n    S-\u003e\u003eVM: vm.loadAll()\n    VM-\u003e\u003eVM: updateLoadingState(true)\n    VM-\u003e\u003eUC: uc.run()\n    UC-\u003e\u003eR: repo.getAll()\n    R-\u003e\u003eAPI: GET /clients\n    API--\u003e\u003eR: ClientModel[]\n    R-\u003e\u003eR: models.map(m =\u003e m.toDomain())\n    R--\u003e\u003eUC: Client[]\n    UC--\u003e\u003eVM: Client[]\n    VM-\u003e\u003eVM: runInAction(() =\u003e isItemsResponse = data)\n    VM-\u003e\u003eVM: updateLoadingState(false)\n    VM--\u003e\u003eS: re-render (observer)\n```\n\n### 1. La pantalla solo bindea inputs y llama a la VM\n\n```tsx\n// src/ui/screens/Clients/ClientsScreen.tsx\nconst ClientsScreen = observer(() =\u003e {\n  const vm = useMemo(\n    () =\u003e container.get\u003cClientsViewModel\u003e(TYPES.ClientsViewModel),\n    [],\n  );\n\n  useEffect(() =\u003e { vm.loadAll(); }, [vm]);\n\n  return (\n    \u003cSafeAreaView\u003e\n      {vm.isClientsLoading \u0026\u0026 \u003cSpinner /\u003e}\n      {vm.isClientsError \u0026\u0026 \u003cText\u003e{vm.isClientsError}\u003c/Text\u003e}\n      \u003cFlatList data={vm.isClientsResponse} ... /\u003e\n      \u003cPrimaryButton label=\"Agregar\" onPress={() =\u003e vm.create(formValues)} /\u003e\n    \u003c/SafeAreaView\u003e\n  );\n});\n```\n\n### 2. La ViewModel orquesta estado y delega en UseCases\n\n```ts\n// src/ui/screens/Clients/ClientsViewModel.ts\n@injectable()\nexport class ClientsViewModel {\n  isClientsLoading = false;\n  isClientsError: string | null = null;\n  isClientsResponse: Client[] | null = null;\n\n  constructor(\n    @inject(TYPES.GetAllClientUseCase) private getAll: GetAllClientUseCase,\n    @inject(TYPES.CreateClientUseCase) private createUC: CreateClientUseCase,\n  ) {\n    makeAutoObservable(this);\n  }\n\n  async loadAll() {\n    this.updateLoadingState(true, null, 'items');\n    try {\n      const response = await this.getAll.run();\n      runInAction(() =\u003e { this.isClientsResponse = response; });\n      this.updateLoadingState(false, null, 'items');\n    } catch (e) { this.handleError(e, 'items'); }\n  }\n\n  // ... updateLoadingState, handleError, create, etc.\n}\n```\n\n### 3. El UseCase ejecuta UNA acción de negocio\n\n```ts\n// src/domain/useCases/GetAllClientUseCase/index.ts\n@injectable()\nexport class GetAllClientUseCase implements UseCase\u003cvoid, Client[]\u003e {\n  constructor(\n    @inject(TYPES.ClientRepository) private repo: ClientRepository,\n  ) {}\n\n  async run(): Promise\u003cClient[]\u003e {\n    return this.repo.getAll();\n  }\n}\n```\n\n### 4. La interfaz del repositorio vive en `domain/`\n\n```ts\n// src/domain/repositories/ClientRepository.ts\nexport interface ClientRepository {\n  getAll(): Promise\u003cClient[]\u003e;\n  create(client: Client): Promise\u003cClient\u003e;\n  // ...\n}\n```\n\n### 5. La implementación vive en `data/` y mapea modelos a entidades\n\n```ts\n// src/data/repositories/ClientRepositoryImpl.ts\n@injectable()\nexport class ClientRepositoryImpl implements ClientRepository {\n  constructor(\n    @inject(TYPES.ClientService) private service: ClientService,\n  ) {}\n\n  async getAll(): Promise\u003cClient[]\u003e {\n    const models = await this.service.fetchAll();\n    return models.map(m =\u003e m.toDomain());\n  }\n}\n```\n\n**Eso es todo.** Cualquier feature nueva sigue ese mismo flujo, escrito por cualquier persona del equipo, queda igual. Esa es la promesa.\n\nVer el flujo completo de archivos en [`examples/`](./examples/).\n\n## Las reglas no negociables\n\n```mermaid\nflowchart LR\n    UI[\"🖥️ UI\u003cbr/\u003eScreens + Components\"]\n    VM[\"🧠 ViewModel\u003cbr/\u003e(MobX + Inversify)\"]\n    UC[\"⚙️ UseCases\u003cbr/\u003e(1 acción = 1 UC)\"]\n    REPO[\"📜 Repository\u003cbr/\u003eInterface (domain)\"]\n    IMPL[\"🔌 Repository\u003cbr/\u003eImpl (data)\"]\n    SVC[\"🌐 Service\u003cbr/\u003eHTTP / Firebase\"]\n\n    UI --\u003e VM\n    VM --\u003e UC\n    UC --\u003e REPO\n    IMPL -.implements.-\u003e REPO\n    IMPL --\u003e SVC\n\n    style UI fill:#1A2F5E,stroke:#2D7EF8,color:#fff\n    style VM fill:#1A2F5E,stroke:#2D7EF8,color:#fff\n    style UC fill:#0A1628,stroke:#2D7EF8,color:#fff\n    style REPO fill:#0A1628,stroke:#2D7EF8,color:#fff\n    style IMPL fill:#0A1628,stroke:#9B59B6,color:#fff\n    style SVC fill:#0A1628,stroke:#9B59B6,color:#fff\n```\n\n1. **UI depende solo del ViewModel.** No importa `data/`, no importa Firebase, no importa axios.\n2. **ViewModel depende solo de UseCases.** No conoce repositorios.\n3. **UseCases dependen solo de contratos del dominio** (interfaces).\n4. **`domain/` no importa nada de framework ni infraestructura.** Es portable.\n5. **Cada acción de negocio = un UseCase.** Uno solo, en su propia carpeta.\n6. **Los modelos de transporte (DTOs) nunca llegan a la UI.** Siempre mapeados a entidades del dominio.\n7. **Las ViewModels son UI-agnósticas.** No tienen `Alert`, ni `navigate`, ni hooks, ni `window`.\n\n## Stack y decisiones\n\n| Pieza               | Elección                            | Alternativa rechazada     | ADR                                                                                  |\n| ------------------- | ----------------------------------- | ------------------------- | ------------------------------------------------------------------------------------ |\n| Estado              | MobX (`makeAutoObservable`)         | Zustand, Redux Toolkit    | [001](./docs/02-decision-records/001-mobx-over-zustand.md)                          |\n| Inyección           | Inversify (`@injectable` + `TYPES`) | React Context, factories  | [002](./docs/02-decision-records/002-inversify-over-context.md)                     |\n| Entidades           | Clases con `[key: string]: any`     | Interfaces puras          | [003](./docs/02-decision-records/003-class-entities-with-index-signature.md)        |\n| Granularidad VM→UC  | 1 acción = 1 UseCase                | Service con N métodos     | [004](./docs/02-decision-records/004-one-usecase-per-action.md)                     |\n| Patrón ViewModel    | `ICalls` + `updateLoadingState`     | `useState` por flag       | [005](./docs/02-decision-records/005-viewmodel-canonical-pattern.md)                |\n\n## Cómo aplicarlo a tu proyecto\n\n1. Lee este README completo y la [filosofía detrás de las decisiones](./docs/00-philosophy.md).\n2. Revisa el [Tour rápido](#tour-rápido-5-min) y los [ejemplos canónicos](./examples/).\n3. Consulta la skill que aplica a tu plataforma:\n   - React Native (Expo): [`skills/react-native/`](./skills/react-native/)\n   - React web: _en Fase 2_\n   - npm package: _en Fase 2_\n4. Para cada feature nueva, sigue el [PR Checklist](./skills/react-native/pr-checklist-clean-architecture.md).\n\nMás detalle en [`docs/01-getting-started.md`](./docs/01-getting-started.md).\n\n## Las skills (uso con LLMs)\n\nLas 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.\n\n| Skill                                                                                              | Propósito                                              |\n| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |\n| [clean-architecture-rn-expo-mvvm](./skills/react-native/clean-architecture-rn-expo-mvvm.md)        | Reglas generales de arquitectura (RN Expo)             |\n| [feature-scaffold-rn](./skills/react-native/feature-scaffold-rn.md)                                | Scaffold completo de una feature vertical              |\n| [design-system-rn](./skills/react-native/design-system-rn.md)                                      | Tokens y componentes del design system                 |\n| [pr-checklist-clean-architecture](./skills/react-native/pr-checklist-clean-architecture.md)        | Checklist para revisar PRs                             |\n\n## FAQ rápido\n\n**¿Por qué MobX en 2026?** Porque `makeAutoObservable` + clases es el match perfecto para MVVM y la VM-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](./docs/02-decision-records/001-mobx-over-zustand.md).\n\n**¿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](./docs/02-decision-records/002-inversify-over-context.md).\n\n**¿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](./docs/02-decision-records/003-class-entities-with-index-signature.md).\n\n**¿\"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](./docs/02-decision-records/004-one-usecase-per-action.md).\n\nMás en [`docs/03-faq.md`](./docs/03-faq.md).\n\n## Roadmap\n\n- ✅ **Fase 1** — Skills RN + docs base + ADRs principales (este release)\n- 🚧 **Fase 2** — Skills React web + monorepo/npm package\n- 🔭 **Fase 3** — Skills Python (FastAPI + hexagonal)\n- 🔭 **Fase 4** — CLI generador de features (`npx cas-cli new-feature Clients`)\n\nDetalle en [`ROADMAP.md`](./ROADMAP.md). Historial de cambios en [`CHANGELOG.md`](./CHANGELOG.md).\n\n## Contribuir\n\nLas 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`](./CONTRIBUTING.md).\n\n## Licencia\n\nMIT — ver [`LICENSE`](./LICENSE).\n\n---\n\n_Escrito por [@Kevinparra535](https://github.com/Kevinparra535). Si esto te ayudó, deja una estrella ⭐ en el repo._\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkevinparra535%2Fclean-architecture-stack","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkevinparra535%2Fclean-architecture-stack","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkevinparra535%2Fclean-architecture-stack/lists"}