Arquitectura de una app Ionic: páginas, navegación y estructura modular

Capítulo 3

Tiempo estimado de lectura: 10 minutos

+ Ejercicio

Capas y piezas principales en una app Ionic (Angular)

En Ionic con Angular, la arquitectura suele girar alrededor de páginas (pantallas), componentes reutilizables (UI compartida), servicios (lógica/estado/acceso a datos) y módulos/estructura de carpetas para mantener el código escalable. La idea clave es organizar por funcionalidades (feature-based) en lugar de agrupar todo por tipo (todas las páginas juntas, todos los servicios juntos), porque así cada funcionalidad queda encapsulada y es más fácil de mantener.

Qué es una “página” en Ionic

Una página es un componente Angular que representa una pantalla completa y normalmente está asociada a una ruta. En Ionic, las páginas se renderizan dentro de <ion-router-outlet> y la navegación se integra con el historial para transiciones nativas.

Qué es un componente reutilizable

Un componente reutilizable encapsula una parte de UI (por ejemplo, un encabezado de sección, una tarjeta de producto, un selector) y se usa en múltiples páginas. Debe ser presentacional cuando sea posible: recibe datos por @Input() y emite eventos por @Output(), evitando depender de rutas o servicios específicos salvo que sea un componente “smart” deliberado.

Servicios y estado

Los servicios suelen concentrar: llamadas HTTP, acceso a almacenamiento, lógica de negocio, y estado compartido (por ejemplo, usuario autenticado, carrito, filtros). Para estado simple, un servicio con BehaviorSubject suele ser suficiente; para flujos complejos, se puede escalar a una librería de estado, pero aquí nos enfocamos en una base mantenible sin añadir dependencias.

Estructura modular por funcionalidades (carpetas recomendadas)

Una estructura práctica y común es separar core (singletons y configuración), shared (reutilizable) y features (funcionalidades). Ejemplo:

Continúa en nuestra aplicación.
  • Escuche el audio con la pantalla apagada.
  • Obtenga un certificado al finalizar.
  • ¡Más de 5000 cursos para que explores!
O continúa leyendo más abajo...
Download App

Descargar la aplicación

src/app/  core/    guards/    interceptors/    services/      auth.service.ts      storage.service.ts    core.module.ts  shared/    components/      app-header/      empty-state/    pipes/    directives/    shared.module.ts  features/    auth/      pages/        login/        register/      auth-routing.module.ts      auth.module.ts    products/      pages/        product-list/        product-detail/      components/        product-card/      services/        products.service.ts      products-routing.module.ts      products.module.ts    tabs/      tabs-routing.module.ts      tabs.page.ts      tabs.page.html  app-routing.module.ts  app.component.ts

Ventajas: cada feature tiene sus páginas, componentes y servicios cerca; core evita duplicar singletons; shared centraliza UI reutilizable.

Cuándo usar Core vs Shared

  • core/: servicios singleton (Auth, Storage), guards, interceptors, configuración global. Se importa una vez.
  • shared/: componentes/pipes/directivas reutilizables y “tontos” (sin estado global). Se importa en múltiples módulos.
  • features/: todo lo específico de una funcionalidad (páginas, componentes, servicios, routing).

Navegación: rutas, parámetros y patrones comunes

RouterOutlet de Ionic y navegación

Ionic usa Angular Router, pero con ion-router-outlet para manejar transiciones y stack de navegación. En general, navegarás con:

  • routerLink en plantillas (recomendado para enlaces simples).
  • Router.navigate() en TypeScript (cuando necesitas lógica).
  • NavController (opcional) para patrones tipo “push/pop” con animaciones; en Angular moderno suele bastar con Router.

Definir rutas por feature (lazy loading)

Para que la app cargue más rápido y sea modular, define rutas con lazy loading por módulo de feature.

// app-routing.module.ts (ejemplo base) const routes: Routes = [  {    path: '',    redirectTo: 'tabs',    pathMatch: 'full',  },  {    path: 'auth',    loadChildren: () => import('./features/auth/auth.module').then(m => m.AuthModule),  },  {    path: 'tabs',    loadChildren: () => import('./features/tabs/tabs-routing.module').then(m => m.TabsPageRoutingModule),  },];

Y dentro de cada feature:

// features/products/products-routing.module.ts const routes: Routes = [  { path: '', component: ProductListPage },  { path: ':id', component: ProductDetailPage },];

Parámetros de ruta (URL) y query params

Usa parámetros de ruta para identificar recursos (por ejemplo, un producto por id) y query params para filtros/estado opcional (por ejemplo, ?q=camisa&sort=price).

Ejemplo de navegación desde una lista:

// product-list.page.ts constructor(private router: Router) {} goToDetail(productId: string) {  this.router.navigate(['/tabs/products', productId], {    queryParams: { ref: 'home' },  }); }

Lectura en el detalle:

// product-detail.page.ts constructor(private route: ActivatedRoute) {} ngOnInit() {  const id = this.route.snapshot.paramMap.get('id');  const ref = this.route.snapshot.queryParamMap.get('ref'); }

Si necesitas reaccionar a cambios (por ejemplo, misma página con distinto id), usa observables:

ngOnInit() {  this.route.paramMap.subscribe(pm => {    const id = pm.get('id');    // cargar datos con id  }); }

Patrones de navegación: Tabs vs Menú lateral

Navegación por Tabs (cuando hay secciones principales)

Los tabs funcionan bien cuando tienes 3–5 secciones principales (Inicio, Buscar, Favoritos, Perfil). En Ionic, normalmente existe una página TabsPage que define las rutas hijas.

// features/tabs/tabs-routing.module.ts const routes: Routes = [  {    path: '',    component: TabsPage,    children: [      {        path: 'products',        loadChildren: () => import('../products/products.module').then(m => m.ProductsModule),      },      {        path: 'profile',        loadChildren: () => import('../profile/profile.module').then(m => m.ProfileModule),      },      { path: '', redirectTo: 'products', pathMatch: 'full' },    ],  },];

En el HTML de tabs:

<ion-tabs>  <ion-tab-bar slot="bottom">    <ion-tab-button tab="products" [routerLink]="['/tabs/products']">      <ion-label>Productos</ion-label>    </ion-tab-button>    <ion-tab-button tab="profile" [routerLink]="['/tabs/profile']">      <ion-label>Perfil</ion-label>    </ion-tab-button>  </ion-tab-bar></ion-tabs>

Menú lateral (cuando hay muchas opciones o jerarquía)

El menú lateral es útil cuando hay muchas secciones o accesos secundarios (Ajustes, Ayuda, Términos). Suele implementarse en app.component.html con ion-menu y un ion-router-outlet principal.

Recomendación: usa menú lateral para navegación “global” y tabs para navegación “principal” dentro de un área. Evita mezclar ambos sin una jerarquía clara.

Protección de rutas (Guards) y control de acceso

Cuando una pantalla requiere autenticación (por ejemplo, Perfil o Checkout), protege la ruta con un guard. La idea: si no hay sesión, redirigir a /auth/login y opcionalmente guardar la URL de retorno.

Servicio de autenticación mínimo (estado)

// core/services/auth.service.ts @Injectable({ providedIn: 'root' }) export class AuthService {  private isLoggedIn$ = new BehaviorSubject<boolean>(false);  isAuthenticated() {    return this.isLoggedIn$.asObservable();  }  get snapshot() {    return this.isLoggedIn$.value;  }  login() { this.isLoggedIn$.next(true); }  logout() { this.isLoggedIn$.next(false); } }

Guard de autenticación

// core/guards/auth.guard.ts @Injectable({ providedIn: 'root' }) export class AuthGuard implements CanActivate {  constructor(private auth: AuthService, private router: Router) {}  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | UrlTree {    if (this.auth.snapshot) return true;    return this.router.createUrlTree(['/auth/login'], {      queryParams: { redirectTo: state.url },    });  } }

Aplicación del guard en rutas:

// features/profile/profile-routing.module.ts const routes: Routes = [  { path: '', component: ProfilePage, canActivate: [AuthGuard] },];

En el login, redirige al destino original:

// login.page.ts constructor(private route: ActivatedRoute, private router: Router, private auth: AuthService) {} onLogin() {  this.auth.login();  const redirectTo = this.route.snapshot.queryParamMap.get('redirectTo') || '/tabs/profile';  this.router.navigateByUrl(redirectTo); }

Manejo del estado de navegación (historial, “back”, y eventos)

Botón atrás coherente

Ionic muestra automáticamente el botón atrás en ion-buttons cuando hay historial. Para controlar el texto/URL de fallback:

<ion-header>  <ion-toolbar>    <ion-buttons slot="start">      <ion-back-button defaultHref="/tabs/products"></ion-back-button>    </ion-buttons>    <ion-title>Detalle</ion-title>  </ion-toolbar></ion-header>

Detectar la URL actual y reaccionar

Para resaltar estados, cerrar menús o disparar analítica, puedes escuchar eventos del Router:

// ejemplo en un componente contenedor constructor(private router: Router) {  this.router.events.pipe(    filter(e => e instanceof NavigationEnd)  ).subscribe((e: any) => {    const currentUrl = e.urlAfterRedirects;    // reaccionar a cambios de ruta  }); }

Preservar estado al volver (lista <-> detalle)

Dos estrategias comunes:

  • Query params para filtros/paginación: /products?q=....
  • Servicio de estado para mantener selección/scroll: guardar filtros y restaurarlos al volver.

Ejemplo simple de estado de filtros:

// features/products/services/products-ui-state.service.ts @Injectable({ providedIn: 'root' }) export class ProductsUiStateService {  private state$ = new BehaviorSubject<{ q: string }>({ q: '' });  setQuery(q: string) { this.state$.next({ q }); }  getState() { return this.state$.asObservable(); }  snapshot() { return this.state$.value; } }

Convenciones de nombres (guía práctica)

ElementoConvenciónEjemplo
Páginaskebab-case en carpeta, sufijo .pageproduct-detail/product-detail.page.ts
Componenteskebab-case en carpeta, sufijo .componentproduct-card/product-card.component.ts
Serviciossufijo .serviceproducts.service.ts
Guardssufijo .guardauth.guard.ts
Módulossufijo .moduleproducts.module.ts
Rutassufijo -routing.moduleproducts-routing.module.ts
Interfaces/TiposPascalCaseProduct, AuthSession

Recomendaciones adicionales:

  • Evita nombres genéricos como utils.ts o helpers.ts; prefiere nombres por dominio: date-format.service.ts, price.pipe.ts.
  • Un componente/página por carpeta, con sus archivos: .ts, .html, .scss, .spec.ts.
  • Importaciones: primero Angular, luego Ionic, luego terceros, luego código local.

Guía paso a paso: pasar de prototipo a estructura mantenible (mini-refactor)

Escenario típico de prototipo: todo vive en src/app/ con páginas sueltas, servicios mezclados y rutas en un solo archivo. Objetivo: mover a features/, crear shared/ y aplicar lazy loading.

Paso 1: Identifica funcionalidades y rutas

Haz una lista de pantallas y agrúpalas por dominio. Ejemplo:

  • Auth: Login, Register
  • Products: ProductList, ProductDetail
  • Profile: Profile
  • Shell: Tabs

Paso 2: Crea carpetas y mueve páginas

Mueve archivos manteniendo el nombre de clase y selector. Por ejemplo:

  • De: src/app/product-list/...
  • A: src/app/features/products/pages/product-list/...

Actualiza imports rotos (Angular lo marcará). Si usas paths largos, considera configurar alias en tsconfig (opcional) para @core, @shared, @features.

Paso 3: Extrae componentes reutilizables a Shared

Si ProductListPage tiene una tarjeta repetida, crea ProductCardComponent en features/products/components si solo se usa ahí, o en shared/components si se reutiliza en varias features.

Ejemplo de componente presentacional:

// shared/components/empty-state/empty-state.component.ts @Component({ selector: 'app-empty-state', templateUrl: './empty-state.component.html' }) export class EmptyStateComponent {  @Input() title = 'Sin datos';  @Input() description = ''; }

Paso 4: Crea módulos y routing por feature

Para cada feature, crea:

  • feature.module.ts (declara páginas/componentes del feature)
  • feature-routing.module.ts (define rutas del feature)

Ejemplo mínimo:

// features/products/products.module.ts @NgModule({  imports: [CommonModule, IonicModule, ProductsRoutingModule],  declarations: [ProductListPage, ProductDetailPage], }) export class ProductsModule {}

Paso 5: Conecta lazy loading desde AppRouting o Tabs

Si Products vive dentro de Tabs, cárgalo como hijo de Tabs (como vimos). Si es una sección independiente, cárgalo desde app-routing.module.ts.

Paso 6: Centraliza singletons en Core

Mueve servicios globales (Auth, Storage, Interceptors, Guards) a core/ y asegúrate de que estén en providedIn: 'root' o en un CoreModule importado una sola vez.

Paso 7: Protege rutas que lo requieran

Agrega canActivate a las rutas privadas y valida el flujo de redirección con redirectTo. Prueba casos:

  • Entrar directo a /tabs/profile sin sesión → debe ir a login.
  • Loguearse → debe volver a /tabs/profile.

Paso 8: Limpia dependencias cruzadas

Señales de alerta:

  • Un servicio de products importando cosas de auth/pages.
  • Componentes shared leyendo ActivatedRoute o navegando.

Refactor típico: pasa datos por @Input y eventos por @Output, y deja la navegación en páginas/containers.

Checklist rápido de arquitectura antes de seguir creciendo

  • ¿Cada feature tiene su propio routing y módulo (lazy loading)?
  • ¿Los componentes shared son reutilizables y sin dependencias de rutas?
  • ¿Los servicios globales viven en core y no se duplican?
  • ¿Las rutas privadas están protegidas con guards y redirección?
  • ¿Los parámetros de ruta y query params se usan con intención (id vs filtros)?
  • ¿La navegación principal está definida (tabs o menú) y no se mezcla sin jerarquía?

Ahora responde el ejercicio sobre el contenido:

¿Cuál es la mejor práctica para organizar el código en una app Ionic con Angular de forma escalable y mantenible?

¡Tienes razón! Felicitaciones, ahora pasa a la página siguiente.

¡Tú error! Inténtalo de nuevo.

La estructura por funcionalidades encapsula cada dominio (páginas, componentes, servicios y rutas) y facilita el mantenimiento. Además, core concentra singletons/configuración y shared la UI reutilizable sin dependencias globales.

Siguiente capítulo

TypeScript aplicado en Ionic: tipado, interfaces y buenas prácticas

Arrow Right Icon
Portada de libro electrónico gratuitaIonic desde Cero: Crea Aplicaciones Híbridas con HTML, CSS y TypeScript
30%

Ionic desde Cero: Crea Aplicaciones Híbridas con HTML, CSS y TypeScript

Nuevo curso

10 páginas

Descarga la aplicación para obtener una certificación gratuita y escuchar cursos en segundo plano, incluso con la pantalla apagada.