Conceptos Básicos de Arquitectura Móvil
10 ejemplos para empezar con arquitectura móvil - 7 básicos y 3 intermedios.
Busca en todas las páginas de la documentación
10 ejemplos para empezar con arquitectura móvil - 7 básicos y 3 intermedios.
Crea una aplicación Expo con forma de producción con un pin explícito de SDK 57. La plantilla default@sdk-57 incluye Expo Router, TypeScript y el diseño de carpetas recomendado para extender.
npx create-expo-app@latest MyApp --template default@sdk-57
cd MyApp
npm installAgrega un árbol src/ junto a app/ para los límites de feature:
MyApp/
├── app/ # Expo Router - solo rutas
├── src/
│ ├── shared/ # UI primitivos, cliente API, configuración
│ ├── entities/ # modelos de dominio (User, Order)
│ └── features/ # rebanadas de producto (auth, orders, settings)
├── app.config.ts
└── package.jsonConfirma el pin de SDK antes de estructurar features:
{
"dependencies": {
"expo": "~57.0.4",
"react": "19.2.3",
"react-native": "0.86.0"
}
}Tooling: Estos ejemplos tienen como objetivo Expo SDK 57 (
expo~57.0.4), React Native 0.86.0 y React 19.2.3.
Las aplicaciones móviles se benefician de un flujo de dependencias simple hacia adentro: presentación (pantallas, componentes) llama dominio (entidades, casos de uso), que llama datos (adaptadores API, almacenamiento).
┌─────────────────────────────────────────┐
│ app/ + features/*/screens, components │ ← presentación
├─────────────────────────────────────────┤
│ entities/ + features/*/model, hooks │ ← dominio
├─────────────────────────────────────────┤
│ shared/api, shared/storage, adapters │ ← datos / infra
└─────────────────────────────────────────┘
dirección de dependencias: ↓ hacia adentro// src/features/orders/screens/OrdersScreen.tsx - presentación orquesta
import { useOrders } from "../hooks/useOrders";
import { OrderList } from "../components/OrderList";
export function OrdersScreen() {
const { orders, loading, error, refresh } = useOrders();
return <OrderList orders={orders} loading={loading} error={error} onRefresh={refresh} />;
}fetch directamente - llama hooks o casos de uso que ocultan IOOrder, User) viven en entities/ para que múltiples features compartan una formaordersApi.ts, secureStorage.ts) se sientan en shared/ o features/*/api/OrderRow presentacional) hace que las pruebas unitarias sean dolorosasRelacionado: Feature-Sliced Design for RN - capas formales de slice adaptadas para Expo | Clean Architecture on Mobile - entidades y casos de uso sin ceremonia
Expo Router mapea nombres de archivo a URLs. Los archivos de ruta deben re-exportar pantallas de feature - no ser dueños de lógica empresarial.
// app/(tabs)/orders/index.tsx - una línea cuando es posible
export { OrdersScreen as default } from "@/features/orders";// src/features/orders/index.ts - API pública para la feature
export { OrdersScreen } from "./screens/OrdersScreen";
export type { Order } from "./model/types";app/ es infraestructura de navegación - el análisis de parámetros y el anidamiento de diseño pertenecen aquí como máximofeatures/orders/index.ts es el único camino de importación que otras features deben usar@/features/orders/components/OrderRow acoplan consumidores a refactores internosRelacionado: ../project-setup/folder-structure-for-features/folder-structure-for-features.md - diseño de rebanada de feature en codebases en crecimiento
shared/ contiene código sin opinión de producto. Las carpetas de feature contienen UI específica del producto y flujos de trabajo.
src/
├── shared/
│ ├── ui/ # Button, Screen, TextField - primitivos del sistema de diseño
│ ├── api/ # createApiClient(), tipos de error
│ ├── config/ # lectores de env (sin secretos en fuente)
│ └── lib/ # formatCurrency, ayudantes de fecha
└── features/
└── checkout/
├── components/ # PaymentSummary - específico de checkout
└── hooks/ # useCheckout - flujo de trabajo de checkout// src/shared/ui/Button.tsx - primitivo, sin conocimiento de dominio
import { Pressable, StyleSheet, Text, type PressableProps } from "react-native";
type ButtonProps = PressableProps & { label: string };
export function Button({ label, style, ...rest }: ButtonProps) {
return (
<Pressable style={[styles.base, style]} {...rest}>
<Text style={styles.label}>{label}</Text>
</Pressable>
);
}
const styles = StyleSheet.create({
base: { paddingVertical: 12, paddingHorizontal: 20, borderRadius: 10, backgroundColor: "#2563eb" },
label: { fontSize: 16, fontWeight: "600", color: "#fff" },
});shared/ cuando dos features no relacionadas lo necesitan y no tiene semántica de productofeatures/checkout/ incluso si solo una pantalla lo usa hoyshared/ui no debe importar desde features/ - las dependencias fluyen en una direccióncomponents/ - las carpetas sin dueño se convierten en magnetos de god-moduleRelacionado: Feature-Sliced Design for RN - reglas de capa
sharedy nombres de segmento
Las entidades describen sustantivos en tu dominio - tipos simples y funciones puras con cero importaciones de React.
// src/entities/order/model/types.ts
export type OrderStatus = "pending" | "shipped" | "delivered" | "cancelled";
export type Order = {
id: string;
title: string;
totalCents: number;
status: OrderStatus;
placedAt: string;
};// src/entities/order/lib/formatOrderTotal.ts
import type { Order } from "../model/types";
export function formatOrderTotal(order: Order): string {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(order.totalCents / 100);
}features/orders y features/tracking usan OrderuseState, sin fetch, sin StyleSheet en entities/ - los mantiene comprobables sin un renderizadormodel/, lib/, api/ cuando la entidad crece más allá de ~200 líneasRelacionado: Clean Architecture on Mobile - cuándo agregar casos de uso alrededor de entidades
Cada carpeta de feature expone un barril index.ts. Las importaciones entre features van solo a través de ese archivo.
// src/features/orders/index.ts
export { OrdersScreen } from "./screens/OrdersScreen";
export { OrderRow } from "./components/OrderRow";
export { useOrders } from "./hooks/useOrders";
export type { OrdersFilter } from "./model/types";// src/features/dashboard/screens/DashboardScreen.tsx - importa solo API pública
import { OrderRow } from "@/features/orders";
import type { Order } from "@/entities/order";normalizeRawOrder) a menos que otra feature realmente los necesiteno-restricted-imports puede hacer cumplir @/features/*/index y bloquear @/features/*/components/*Relacionado: Refactoring Checklist - elementos de auditoría para violaciones de límites antes de la versión
La red y el almacenamiento IO viven detrás de adaptadores para que las pantallas intercambien implementaciones (mock, staging, prod) sin ediciones.
// src/shared/api/createApiClient.ts
type ApiClient = {
get<T>(path: string): Promise<T>;
post<T>(path: string, body: unknown): Promise<T>;
};
export function createApiClient(baseUrl: string): ApiClient {
return {
async get<T>(path: string) {
const response = await fetch(`${baseUrl}${path}`);
if (!response.ok) throw new Error(`GET ${path} failed: ${response.status}`);
return response.json() as Promise<T>;
},
async post<T>(path: string, body: unknown) {
const response = await fetch(`${baseUrl}${path}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!response.ok) throw new Error(`POST ${path} failed: ${response.status}`);
return response.json() as Promise<T>;
},
};
}// src/features/orders/api/ordersApi.ts
import type { Order } from "@/entities/order";
import type { ApiClient } from "@/shared/api/createApiClient";
export function createOrdersApi(client: ApiClient) {
return {
list: () => client.get<Order[]>("/orders"),
byId: (id: string) => client.get<Order>(`/orders/${id}`),
};
}createOrdersApi) aceptan dependencias - fácil de inyectar mocks en pruebasfetch crudas no deben aparecer en componentes de pantallaordersApi, authApi) vence a un único god api.tsRelacionado: Dependency Injection in RN - cableado de adaptadores sin un framework de DI
La configuración del tiempo de ejecución fluye a través de un único lector - no dispersa llamadas de process.env en código de feature.
// src/shared/config/env.ts
import Constants from "expo-constants";
type AppConfig = {
apiBaseUrl: string;
appVariant: "development" | "staging" | "production";
};
export function getAppConfig(): AppConfig {
const extra = Constants.expoConfig?.extra as Partial<AppConfig> | undefined;
return {
apiBaseUrl: extra?.apiBaseUrl ?? "https://api.staging.example.com",
appVariant: extra?.appVariant ?? "development",
};
}// app.config.ts (extracto)
export default {
extra: {
apiBaseUrl: process.env.EXPO_PUBLIC_API_URL,
appVariant: process.env.APP_VARIANT ?? "development",
},
};EXPO_PUBLIC_* vars se incrustan en el bundle de JS - nunca pongas secretos allígetAppConfig() es el único módulo que lee Constants.expoConfig?.extra para configuración de aplicacióngetAppConfig() o reciben config a través de context - no leen env directamenteenv por canal - ver guías de entorno de project-setupRelacionado: Modular Monolith vs Multi-App - un binario con variantes vs aplicaciones separadas
Las features no deben importar internos entre sí. Usa entidades compartidas, eventos u orquestación en la capa de aplicación.
// src/features/cart/hooks/useCart.ts - cart es dueño de su state
import { create } from "zustand";
type CartItem = { productId: string; qty: number };
type CartStore = {
items: CartItem[];
add: (item: CartItem) => void;
clear: () => void;
};
export const useCart = create<CartStore>((set) => ({
items: [],
add: (item) => set((s) => ({ items: [...s.items, item] })),
clear: () => set({ items: [] }),
}));// app/(tabs)/catalog/[id].tsx - ruta orquesta navegación + cart, no features importando features
import { useLocalSearchParams, router } from "expo-router";
import { ProductDetailScreen } from "@/features/catalog";
import { useCart } from "@/features/cart";
export default function ProductRoute() {
const { id } = useLocalSearchParams<{ id: string }>();
const add = useCart((s) => s.add);
return (
<ProductDetailScreen
productId={id ?? ""}
onAddToCart={(productId) => {
add({ productId, qty: 1 });
router.push("/(tabs)/cart");
}}
/>
);
}features/catalog ignorante de navegación de cartonAddToCart) a pantallas en lugar de importar stores de features hermanasProduct, Order) viven en entities/ - ambas features importan esosshared/events - no importaciones de store directoRelacionado: ADR: State Management Selection - cuándo las stores globales son justificadas
La arquitectura se paga a sí misma cuando cada capa prueba sin arrancar la aplicación completa.
// src/features/orders/hooks/useOrders.test.ts
import { renderHook, waitFor } from "@testing-library/react-native";
import { useOrders } from "./useOrders";
import type { Order } from "@/entities/order";
const mockOrders: Order[] = [
{ id: "1", title: "Widget", totalCents: 999, status: "pending", placedAt: "2026-01-01" },
];
jest.mock("../api/ordersApi", () => ({
ordersApi: { list: jest.fn(() => Promise.resolve(mockOrders)) },
}));
it("loads orders on mount", async () => {
const { result } = renderHook(() => useOrders());
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.orders).toHaveLength(1);
});// src/entities/order/lib/formatOrderTotal.test.ts - puro, sin renderizador
import { formatOrderTotal } from "./formatOrderTotal";
it("formats cents as USD", () => {
expect(
formatOrderTotal({ id: "1", title: "A", totalCents: 2500, status: "pending", placedAt: "" })
).toBe("$25.00");
});fetch globalmenteRelacionado: Dependency Injection in RN - servicios inyectables para test doubles
Permanece en un único repo hasta que dos aplicaciones o aislamiento de CI fuerce un paquete de workspace.
monorepo/
├── apps/
│ └── mobile/ # Aplicación Expo - importa @acme/orders
├── packages/
│ ├── orders/ # entidad + tipos de API compartidos con admin web
│ └── ui/ # sistema de diseño
└── pnpm-workspace.yaml// apps/mobile/package.json
{
"dependencies": {
"@acme/orders": "workspace:*",
"expo": "~57.0.4",
"react": "19.2.3",
"react-native": "0.86.0"
}
}// packages/orders/src/index.ts
export type { Order, OrderStatus } from "./model/types";
export { formatOrderTotal } from "./lib/formatOrderTotal";npx expo-doctor después de agregar deps de workspace - Metro debe resolver el symlinksrc/entities/ en la aplicación es gratuito hasta que existe un segundo consumidorRelacionado: ../project-setup/shared-packages-and-metro-resolution/shared-packages-and-metro-resolution.md - paquetes de Metro y workspace
Versiones de Stack: Esta página fue escrita para React 19.2.3, React Native 0.86.0 y Expo SDK 57 (
expo~57.0.4).
Revisado por Chris St. John·Última actualización: 16 jul 2026