<link rel="stylesheet" href="/assets/fonts/jetbrains-mono/jetbrains-mono.css" />
All posts

Arquitectura Angular

Crear una aplicación Angular que funcione es relativamente simple. Crear una aplicación Angular que se mantenga ordenada, legible, escalable y fácil de mantener después de meses o años de desarrollo es otra cosa completamente distinta. La verdadera diferencia entre un proyecto amateur y un proyecto profesional radica no sólo en el código que "hace su trabajo", sino en la arquitectura que permite al equipo seguir desarrollándose sin crear caos.

Cuando una aplicación crece, aumentan las páginas, componentes, servicios, llamadas HTTP, estados a administrar, permisos, rutas, formularios, validaciones y dependencias entre las distintas partes del sistema. Si todo se coloca en carpetas genéricas como componentes, servicios y modelos, al poco tiempo resulta difícil entender dónde se encuentra una característica, quién usa qué y qué partes del código se pueden cambiar sin alterar el resto.

En esta guía vemos cómo estructurar una aplicación Angular moderna usando una arquitectura basada en características, una clara separación entre core y shared, los componentes independientes, los señales, el patrón componentes inteligentes vs tontos y una estructura de carpetas diseñada para proyectos reales.


Por qué la arquitectura es importante en Angular

Angular es un marco muy poderoso porque ya ofrece una estructura clara: componentes, servicios, inyección de dependencia, enrutamiento, formularios, cliente HTTP, guardia, interceptor y mucho más. Sin embargo, precisamente porque Angular ofrece tantas herramientas, es fácil usarlas sin una estrategia coherente.

Un proyecto pequeño puede sobrevivir incluso con una estructura menos ordenada. Sin embargo, un proyecto mediano o grande necesita reglas. Sin reglas, cada desarrollador organiza el código a su manera, los componentes se vuelven demasiado grandes, los servicios acumulan responsabilidades no relacionadas y el proyecto poco a poco se convierte en un bloque difícil de cambiar.

Una buena arquitectura Angular debe ayudar a conseguir algunos objetivos fundamentales:

  • Escalabilidad: agregue nuevas funciones sin tener que reorganizar todo el proyecto.
  • Mantenibilidad: comprenda rápidamente dónde está el código y cómo cambiarlo.
  • Separación de responsabilidades: Cada archivo, componente o servicio debe tener una función clara.
  • Testability: El código debe ser fácil de probar de forma aislada.
  • Rendimiento: la estructura debe favorecer la carga diferida, paquetes más pequeños y un renderizado eficiente.
  • Colaboración: varios desarrolladores deben poder trabajar en la misma aplicación sin pisarse unos a otros.

El punto central es este: la arquitectura no sirve para complicar el proyecto, sino para hacerlo más predecible. Cuando una estructura es predecible, cada característica nueva tiene un lugar natural para vivir.


Arquitectura basada en funciones: organizar el código por función

Uno de los errores más comunes en proyectos Angular es organizar el código por tipo técnico en lugar de por dominio funcional. Una estructura como esta puede parecer ordenada al principio:

src/app/
  components/
  services/
  models/
  pipes/
  directives/
  pages/

El problema es que esta organización no dice nada sobre el producto. Si está trabajando en la sección de proyectos, deberá buscar componentes en componentes, servicios en servicios, modelos en modelos, páginas en páginas, etc. Cada característica está dispersa por todo el proyecto.

A arquitectura basada en características, por otro lado, organiza el código en torno a la funcionalidad de la aplicación. Por ejemplo:

src/app/
  features/
    dashboard/
    blog/
    projects/
    experiences/
    auth/
    admin/

Cada carpeta representa una parte real del producto. Todo lo relacionado con el blog está dentro de features/blog. Todo lo relacionado con proyectos está dentro de características/proyectos. Todo lo relacionado con el administrador está dentro de features/admin.

Este enfoque mejora enormemente la legibilidad del proyecto. Cuando necesitas cambiar una función, sabes a dónde ir. Cuando necesita eliminar una función, sabe qué archivos se ven afectados. Cuando un nuevo desarrollador se une al equipo, puede comprender la aplicación a partir de las funciones y no de las carpetas técnicas genéricas.

Una característica debe ser lo más autónoma posible

Una buena característica de Angular debe contener todo lo que necesita para funcionar: páginas, componentes específicos, servicios de acceso a datos, modelos, tiendas locales, rutas y utilidades internas.

Ejemplo de estructura para una característica proyectos:

src/app/features/projects/
  projects.routes.ts
  pages/
    projects-list-page.component.ts
    project-detail-page.component.ts
  components/
    project-card.component.ts
    project-filters.component.ts
    project-empty-state.component.ts
  data-access/
    projects-api.service.ts
    projects-store.service.ts
  models/
    project.model.ts
    project-filter.model.ts
  utils/
    project-status.util.ts

Esta estructura hace que la función sea independiente y fácil de entender. Las páginas están separadas por componentes más pequeños, la lógica de acceso a datos está en data-access, los tipos de TypeScript están en models y las funciones auxiliares específicas de funciones están en utils.

Regla importante: evitar dependencias entre funciones

Una característica no debe importar directamente componentes, servicios o modelos de otra característica. Por ejemplo, features/blog no debería importar código de features/projects. Esto crea un acoplamiento y dificulta la modificación de una característica sin afectar a las demás.

Si dos funciones necesitan el mismo componente o utilidad, es probable que ese elemento deba trasladarse a shared. Sin embargo, si comparten una lógica de dominio importante, puede resultar útil crear una carpeta o biblioteca dedicada, pero siempre con límites claros.


Core y Shared: diferencia fundamental

En muchos proyectos de Angular encontramos las carpetas core y shared, pero muchas veces se usan mal. Comprender la diferencia entre estas dos áreas es esencial para mantener limpio su proyecto.

Core: lo que pertenece a la aplicación global

La carpeta core contiene código global, utilizado a nivel de aplicación, que a menudo se inicializa solo una vez. Aquí encontramos elementos como autenticación, interceptores HTTP, guardias globales, diseño principal, manejo de errores, configuraciones, servicios singleton y lógica de infraestructura.

Ejemplo:

src/app/core/
  auth/
    auth.service.ts
    auth.guard.ts
    auth.interceptor.ts
  http/
    api-error.interceptor.ts
    http-context.tokens.ts
  layout/
    main-layout.component.ts
    admin-layout.component.ts
  config/
    app-config.token.ts
  guards/
    role.guard.ts
  services/
    logger.service.ts
    storage.service.ts

El core no debería convertirse en un vertedero de servicios aleatorios. Debe contener sólo lo que es verdaderamente global. Si un servicio solo ofrece la función de blog, no ingresa en core: ingresa en features/blog/data-access.

Compartido: lo que es reutilizable y sin lógica de negocio específica

La carpeta shared contiene elementos que se pueden reutilizar en varias partes de la aplicación, pero no vinculados a una función específica. Aquí encontramos componentes genéricos de UI, canalizaciones, directivas, ayudantes y plantillas verdaderamente compartidas.

src/app/shared/
  ui/
    button/
    modal/
    card/
    badge/
    spinner/
  pipes/
    truncate.pipe.ts
    safe-html.pipe.ts
  directives/
    autofocus.directive.ts
    click-outside.directive.ts
  utils/
    date-format.util.ts
    string.util.ts
  models/
    pagination.model.ts
    api-response.model.ts

Un componente como app-button, app-modal o app-spinner puede caber en shared/ui. Sin embargo, un componente como project-card no debe compartirse porque pertenece al dominio de proyectos.

Error común: poner todo en Compartido

Uno de los antipatrones más frecuentes es crear una enorme carpeta shared que contiene todo. Al principio parece conveniente, pero después de unos meses resulta imposible entender qué componentes son verdaderamente genéricos y cuáles se colocaron ahí sólo por conveniencia.

La regla general es simple: si un componente contiene palabras, lógica o conceptos relacionados con una característica específica, no se comparte. Si por el contrario es genérico, reutilizable e independiente del dominio, puede pasar a compartido.


Componentes independientes: Angular moderno sin NgModules innecesarios

Con Angular moderno, los componentes independientes se han convertido en la forma recomendada de crear aplicaciones más simples, explícitas y modulares. Anteriormente, cada componente debía declararse dentro de un NgModule. Esto a menudo daba como resultado formas muy grandes o poco claras.

Con componentes independientes, cada componente declara directamente sus dependencias a través de la propiedad imports. Esto hace que el código sea más legible: al observar un componente, puedes comprender inmediatamente qué otros componentes, directivas o canalizaciones utiliza.

@Component({
  selector: 'app-project-card',
  standalone: true,
  imports: [DatePipe],
  template: `
    <article class="project-card">
      <h3>{{ project().title }}</h3>
      <p>{{ project().description }}</p>
      <small>Pubblicato il {{ project().createdAt | date }}</small>
      <button type="button" (click)="open.emit(project().id)">
        Apri progetto
      </button>
    </article>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProjectCardComponent {
  project = input.required<Project>();
  open = output<string>();
}

Este enfoque tiene varias ventajas:

  • las dependencias son explícitas;
  • los componentes son más fáciles de mover y probar;
  • la carga diferida se vuelve más natural;
  • se reduce la necesidad de módulos intermedios;
  • el proyecto se siente más ligero mentalmente.

Arranque de aplicación independiente

En una aplicación Angular moderna, el arranque se puede manejar sin un AppModule tradicional. La configuración global a menudo se define en app.config.ts.

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes, withComponentInputBinding()),
    provideHttpClient(withInterceptors([
      authInterceptor,
      apiErrorInterceptor
    ]))
  ]
};

Y en el archivo main.ts:

bootstrapApplication(AppComponent, appConfig)
  .catch((error) => console.error(error));

Esta configuración separa claramente el punto de inicio de la aplicación de la lógica de la función.


Enrutamiento y carga diferida para funciones

El enrutamiento es una parte central de la arquitectura Angular. En un proyecto escalable, cada característica debe tener sus propias rutas, cargadas de forma diferida cuando sea posible.

En el archivo principal app.routes.ts solo podemos definir las rutas principales:

export const routes: Routes = [
  {
    path: '',
    loadComponent: () =>
      import('./features/home/pages/home-page.component')
        .then((m) => m.HomePageComponent)
  },
  {
    path: 'projects',
    loadChildren: () =>
      import('./features/projects/projects.routes')
        .then((m) => m.PROJECTS_ROUTES)
  },
  {
    path: 'blog',
    loadChildren: () =>
      import('./features/blog/blog.routes')
        .then((m) => m.BLOG_ROUTES)
  },
  {
    path: 'admin',
    canMatch: [authGuard],
    loadChildren: () =>
      import('./features/admin/admin.routes')
        .then((m) => m.ADMIN_ROUTES)
  }
];

Dentro de la función proyectos, sin embargo, definimos las rutas específicas:

export const PROJECTS_ROUTES: Routes = [
  {
    path: '',
    loadComponent: () =>
      import('./pages/projects-list-page.component')
        .then((m) => m.ProjectsListPageComponent)
  },
  {
    path: ':id',
    loadComponent: () =>
      import('./pages/project-detail-page.component')
        .then((m) => m.ProjectDetailPageComponent)
  }
];

Este enfoque mantiene limpio el archivo de rutas principales y permite que Angular cargue solo el código necesario para la sección visitada por el usuario.


Señales: gestión del estado más sencilla y con mayor capacidad de respuesta

Las señales son una de las herramientas más importantes del Angular moderno. Le permiten gestionar el estado local y derivado de una manera más sencilla, legible y eficiente que muchas soluciones tradicionales.

Una señal representa un valor reactivo. Cuando el valor cambia, Angular sabe qué partes de la interfaz de usuario deben actualizarse. Esto hace que el Estado sea más explícito y reduce la complejidad en muchos escenarios.

Ejemplo sencillo:

const count = signal(0);

const double = computed(() => count() * 2);

function increment(): void {
  count.update((value) => value + 1);
}

En una aplicación escalable, las señales se pueden utilizar en diferentes niveles:

  • estado local del componente, como pestaña activa, filtros, abriendo un modal;
  • estado derivado, como elementos totales, elementos filtrados, estado vacío;
  • tienda de funciones, cuando una sección de la aplicación tiene datos compartidos entre varios componentes;
  • fachada, para exponer un estado simple y listo para usar en la interfaz de usuario.

Ejemplo de tienda con señales

Una buena práctica es crear una tienda específica para la característica, evitando poner toda la lógica dentro de los componentes.

@Injectable()
export class ProjectsStore {
  private readonly api = inject(ProjectsApiService);

  private readonly _projects = signal<Project[]>([]);
  private readonly _loading = signal(false);
  private readonly _error = signal<string | null>(null);
  private readonly _query = signal('');

  readonly projects = this._projects.asReadonly();
  readonly loading = this._loading.asReadonly();
  readonly error = this._error.asReadonly();
  readonly query = this._query.asReadonly();

  readonly filteredProjects = computed(() => {
    const query = this._query().toLowerCase().trim();

    if (!query) {
      return this._projects();
    }

    return this._projects().filter((project) =>
      project.title.toLowerCase().includes(query)
    );
  });

  readonly total = computed(() => this.filteredProjects().length);

  async loadProjects(): Promise<void> {
    this._loading.set(true);
    this._error.set(null);

    try {
      const projects = await firstValueFrom(this.api.getProjects());
      this._projects.set(projects);
    } catch {
      this._error.set('Impossibile caricare i progetti.');
    } finally {
      this._loading.set(false);
    }
  }

  setQuery(query: string): void {
    this._query.set(query);
  }
}

Esta tienda tiene una responsabilidad clara: gestionar el estado de la función de proyectos. El componente no necesita saber cómo se cargan los datos ni cómo se filtran. Solo necesita leer señales y llamar a métodos expuestos por la tienda.

Dónde proporcionar una tienda de funciones

Si la tienda solo ofrece una función específica, se puede proporcionar a nivel de ruta o componente. De esta manera, su ciclo de vida está vinculado a la característica misma y no permanece global innecesariamente.

export const PROJECTS_ROUTES: Routes = [
  {
    path: '',
    providers: [ProjectsStore, ProjectsApiService],
    loadComponent: () =>
      import('./pages/projects-list-page.component')
        .then((m) => m.ProjectsListPageComponent)
  }
];

Esta opción evita llenar el inyector raíz con servicios que no necesitan estar activos durante toda la vida útil de la aplicación.

Signals y RxJS: no son enemigos

Un error común es pensar que las señales reemplazan completamente a RxJS. En realidad, las dos herramientas pueden coexistir muy bien. Las señales son excelentes para estados sincrónicos, derivaciones y estados de UI. RxJS sigue siendo muy útil para transmisiones asincrónicas, eventos complejos, WebSockets, antirrebote, reintentos, combinaciones de transmisiones y cancelación automática de solicitudes.

Una regla práctica útil:

  • usa señales para representar el estado actual de la interfaz de usuario;
  • use calculado para valores derivados;
  • use RxJS cuando necesite modelar flujos asincrónicos en el tiempo;
  • use conversiones como toSignal cuando desee exponer un Observable a la plantilla más fácilmente.

Componentes inteligentes versus tontos

El patrón componentes inteligentes vs tontos es uno de los más útiles para mantener una aplicación Angular limpia. La idea es separar los componentes que manejan lógica, datos y comunicación de aquellos que solo se ocupan de mostrar información.

Componentes inteligentes

Los componentes inteligentes, también llamados componentes contenedores, son componentes que reconocen funciones. Pueden utilizar servicios, tiendas, enrutadores, parámetros de ruta y lógica de aplicación. Suelen corresponder a una página o a un componente principal del artículo.

Ejemplo de componentes inteligentes:

@Component({
  selector: 'app-projects-list-page',
  standalone: true,
  imports: [
    ProjectCardComponent,
    ProjectFiltersComponent,
    SpinnerComponent
  ],
  template: `
    <section>
      <h2>Progetti</h2>

      <app-project-filters
        [query]="store.query()"
        (queryChange)="store.setQuery($event)"
      />

      @if (store.loading()) {
        <app-spinner />
      } @else if (store.error()) {
        <p class="error">{{ store.error() }}</p>
      } @else {
        <div class="projects-grid">
          @for (project of store.filteredProjects(); track project.id) {
            <app-project-card
              [project]="project"
              (open)="openProject($event)"
            />
          }
        </div>
      }
    </section>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProjectsListPageComponent implements OnInit {
  readonly store = inject(ProjectsStore);
  private readonly router = inject(Router);

  ngOnInit(): void {
    this.store.loadProjects();
  }

  openProject(id: string): void {
    this.router.navigate(['/projects', id]);
  }
}

Este componente es inteligente porque coordina la página: carga los datos, lee la tienda, gestiona la navegación y pasa información a los componentes secundarios.

Componentes tontos

Los componentes tontos, también llamados componentes de presentación, son componentes simples, reutilizables y fáciles de probar. Reciben datos a través de entrada y emiten eventos a través de salida. No deberían saber nada sobre enrutadores, servicios HTTP o tiendas globales.

@Component({
  selector: 'app-project-filters',
  standalone: true,
  template: `
    <label>
      Cerca progetto
      <input
        type="search"
        [value]="query()"
        (input)="onInput($event)"
        placeholder="Cerca per titolo..."
      />
    </label>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProjectFiltersComponent {
  query = input('');
  queryChange = output<string>();

  onInput(event: Event): void {
    const target = event.target as HTMLInputElement;
    this.queryChange.emit(target.value);
  }
}

Este componente no sabe nada acerca de la función completa. No sabe de dónde vienen los proyectos, no conoce la API, no navega, no modifica directamente la tienda. Su trabajo es simplemente mostrar una entrada y comunicar el nuevo valor.

Por qué funciona esta separación

Separar componentes inteligentes y tontos aporta ventajas concretas:

  • los componentes de presentación son más fáciles de reutilizar;
  • las pruebas se vuelven más fáciles;
  • la lógica de la aplicación permanece en los contenedores o almacenes;
  • la interfaz de usuario se vuelve más predecible;
  • reduce el riesgo de componentes enormes y difíciles de mantener.

Sin embargo, no debemos aplicar el patrón de manera dogmática. Para componentes muy pequeños o funciones simples, un solo componente puede ser suficiente. El objetivo no es crear tantos expedientes como sea posible, sino mantener responsabilidades claras.


Estructura de carpetas recomendada para una aplicación Angular escalable

Una estructura sólida para una aplicación Angular moderna podría verse así:

src/
  app/
    app.component.ts
    app.config.ts
    app.routes.ts

    core/
      auth/
        auth.service.ts
        auth.guard.ts
        auth.interceptor.ts
      http/
        api-error.interceptor.ts
      layout/
        main-layout.component.ts
        admin-layout.component.ts
      services/
        logger.service.ts
        storage.service.ts

    shared/
      ui/
        button/
          button.component.ts
        modal/
          modal.component.ts
        spinner/
          spinner.component.ts
      pipes/
        truncate.pipe.ts
      directives/
        autofocus.directive.ts
      models/
        pagination.model.ts
        api-response.model.ts
      utils/
        date.util.ts

    features/
      home/
        pages/
          home-page.component.ts

      projects/
        projects.routes.ts
        pages/
          projects-list-page.component.ts
          project-detail-page.component.ts
        components/
          project-card.component.ts
          project-filters.component.ts
        data-access/
          projects-api.service.ts
          projects-store.service.ts
        models/
          project.model.ts
        utils/
          project-status.util.ts

      blog/
        blog.routes.ts
        pages/
          blog-list-page.component.ts
          blog-detail-page.component.ts
        components/
          blog-card.component.ts
          blog-search.component.ts
        data-access/
          blog-api.service.ts
          blog-store.service.ts
        models/
          blog-post.model.ts

      admin/
        admin.routes.ts
        pages/
          admin-dashboard-page.component.ts
          edit-post-page.component.ts
        components/
          admin-sidebar.component.ts
          content-editor.component.ts
        data-access/
          admin-api.service.ts
          admin-store.service.ts
        models/
          admin-user.model.ts

  environments/
    environment.ts

Este marco es lo suficientemente simple como para comprenderlo rápidamente, pero lo suficientemente sólido como para crecer con el tiempo.

Carpetas maestras explicadas

app.config.ts contiene proveedores globales: enrutador, cliente HTTP, interceptor, configuraciones globales y servicios de aplicaciones.

app.routes.ts contiene solo las rutas principales y delega rutas detalladas a funciones individuales.

core contiene lo que existe globalmente: autenticación, interceptor, diseño, registrador, configuraciones y servicios de infraestructura.

shared contiene elementos genéricos y reutilizables, sin lógica empresarial específica.

features contiene el corazón de la aplicación, organizado por dominio funcional.

pages contiene componentes vinculados directamente a rutas. Suelen ser componentes inteligentes.

components contiene componentes de funciones específicas, a menudo tontos o semipresentativos.

data-access contiene servicios API, tienda, fachada y lógica de acceso a datos.

models contiene interfaces y tipos de TypeScript relacionados con la función.

utils contiene funciones puras específicas de la característica.


Capa de acceso a datos: aislar API y estado

En una aplicación Angular escalable, los componentes no deben comunicarse directamente con HttpClient. Si cada componente realiza llamadas HTTP autónomas, la lógica se duplica y resulta difícil gestionar la carga, los errores, el almacenamiento en caché y la transformación de datos.

Es mejor crear una capa data-access para cada característica. Este nivel puede contener:

  • Servicios API;
  • almacena según señales;
  • fachada;
  • mapeador entre modelos DTO y UI;
  • cuenta con lógica de almacenamiento en caché local.

Servicio API de ejemplo:

@Injectable()
export class ProjectsApiService {
  private readonly http = inject(HttpClient);
  private readonly baseUrl = '/api/projects';

  getProjects(): Observable<Project[]> {
    return this.http.get<Project[]>(this.baseUrl);
  }

  getProjectById(id: string): Observable<Project> {
    return this.http.get<Project>(`${this.baseUrl}/${id}`);
  }
}

El componente no necesita conocer puntos finales, URL o detalles HTTP. Debe comunicarse con la tienda o con una fachada. Esto mantiene limpia la interfaz de usuario y facilita las pruebas.


Reglas generales sobre adicción

Para evitar el caos arquitectónico, es útil establecer reglas claras sobre las dependencias:

  • Una característica se puede importar desde compartido, porque compartido contiene elementos genéricos.
  • Una función puede utilizar servicios principales, como autenticación o registrador, si es realmente necesario.
  • Shared no debe importar características, de lo contrario deja de ser genérico.
  • Shared debe evitar fuertes dependencias del núcleo, para seguir siendo reutilizable.
  • Una característica no debe importar directamente otra característica.
  • Core no debe contener lógica específica de una sola característica.

Una posible dirección de las dependencias es esta:

features  --->  shared
features  --->  core
core      --->  shared
shared    --->  nessuna feature

Cuanto más se respeten estas reglas, más modular seguirá siendo la aplicación.


Convenciones de nomenclatura recomendadas

Las convenciones de nomenclatura parecen detalles, pero en proyectos grandes marcan una gran diferencia. Un nombre coherente reduce el tiempo necesario para comprender la función de un archivo.

  • *.page.ts para componentes conectados a una ruta.
  • *.component.ts para componentes de interfaz de usuario normales.
  • *.service.ts para servicios generales.
  • *.api.service.ts para servicios que se comunican con el backend.
  • *.store.ts o *.store.service.ts para conocer el estado de la función.
  • *.guard.ts para guardia de ruta.
  • *.interceptor.ts para interceptor HTTP.
  • *.model.ts para interfaces y tipos principales.
  • *.util.ts para funciones puramente de soporte.

Por ejemplo, project-detail-page.component.ts comunica inmediatamente que es una página. projects-api.service.ts comunica que ese servicio habla con el backend. projects-store.service.ts comunica que ese archivo administra el estado de la característica.


Rendimiento y arquitectura

Una buena arquitectura Angular no se trata solo del orden de los archivos. También afecta el rendimiento de la aplicación.

Usar funciones de carga diferida significa evitar cargar todo el código al inicio. Si un usuario solo visita la página de inicio, no tiene sentido descargar también inmediatamente el código para el administrador, el editor, el panel y todas las secciones internas.

Los componentes independientes ayudan porque facilitan la carga de componentes y rutas de forma granular. Las señales ayudan porque hacen que la representación sea más precisa y el estado sea más fácil de rastrear. El patrón inteligente/tonto ayuda porque reduce los componentes enormes y las plantillas difíciles de optimizar.

Algunas buenas prácticas:

  • use carga diferida para funciones que no se necesitan inmediatamente;
  • use ChangeDetectionStrategy.OnPush en componentes;
  • use @for con track para listas de intérpretes;
  • mantenga los componentes pequeños y enfocados;
  • evita la lógica pesada directamente en la plantilla;
  • usa señales y calcula valores derivados;
  • cargar imágenes y activos de forma optimizada;
  • separe el administrador de la interfaz pública siempre que sea posible.

Errores comunes que se deben evitar

1. Componentes demasiado grandes

Un componente que contiene plantillas enormes, llamadas HTTP, manejo de formularios, estado, validaciones, mapeo de datos y lógica de navegación rápidamente se vuelve inmanejable. Si un miembro excede demasiadas responsabilidades, es hora de dividirlas.

2. Servicios globales para todo

No todos los servicios necesitan ser proporcionados en: 'root'. Si un servicio solo ofrece una función, puede ser más apropiado proporcionarlo a nivel de ruta o componente. Esto reduce el estado global innecesario y hace que el ciclo de vida sea más predecible.

3. Compartido demasiado grande

Una carpeta compartida enorme suele ser un signo de una arquitectura débil. Shared debe contener componentes y utilidades verdaderamente genéricos, no fragmentos de funciones incluidas por conveniencia.

4. Funciones emparejadas entre sí

Cuando una función importa directamente archivos de otra función, el proyecto se vuelve frágil. Es mejor colocar el código compartido en un área común con una responsabilidad clara.

5. Lógica empresarial en plantilla

La plantilla debe permanecer legible. Si una condición se vuelve demasiado compleja, muévala a un método claro y calculado o a un almacén de características.

6. Falta de convenciones

Si cada desarrollador crea carpetas y nombres a su manera, el proyecto pierde coherencia. Los acuerdos deben ser sencillos, documentados y respetados.


Ejemplo práctico: una función de blog bien estructurada

Imaginemos una sección de blog con lista de artículos, detalles de artículos, búsqueda, filtros de categorías y gestión de contenido en el lado del administrador. Una estructura ordenada podría ser:

src/app/features/blog/
  blog.routes.ts

  pages/
    blog-list-page.component.ts
    blog-detail-page.component.ts

  components/
    blog-card.component.ts
    blog-search.component.ts
    blog-category-filter.component.ts
    blog-empty-state.component.ts

  data-access/
    blog-api.service.ts
    blog-store.service.ts

  models/
    blog-post.model.ts
    blog-category.model.ts

  utils/
    reading-time.util.ts
    slug.util.ts

La página blog-list-page es inteligente: usa la tienda, carga datos, administra filtros y busca. El componente blog-card es tonto: recibe un artículo y muestra el título, el extracto, la imagen y la fecha. El servicio blog-api se comunica con el backend. La tienda mantiene estado, carga, errores y artículos filtrados.

Este tipo de separación le permite cambiar el diseño de la tarjeta sin tocar la lógica de datos, o cambiar los puntos finales de API sin cambiar los componentes de presentación.


Lista de verificación final para una arquitectura angular escalable

Antes de considerar sólida la estructura de su aplicación Angular, puede usar esta lista de verificación:

  • Las funciones principales están organizadas en características?
  • ¿Cada característica tiene rutas, páginas, componentes, acceso a datos y modelos separados?
  • ¿El código global está realmente limitado a core?
  • shared contiene solo elementos genéricos y reutilizables?
  • ¿Las funciones evitan dependencias directas entre sí?
  • ¿Las rutas principales utilizan carga diferida?
  • ¿Los componentes independientes declaran claramente sus dependencias?
  • ¿La lógica HTTP está fuera de los componentes?
  • ¿El estado del elemento es gestionado por tiendas, fachadas o servicios dedicados?
  • ¿Se utilizan señales para el estado local y los valores derivados?
  • ¿Se utiliza RxJS donde realmente se necesita modelar flujos asincrónicos?
  • ¿Los componentes inteligentes y tontos tienen responsabilidades separadas?
  • ¿Son consistentes los nombres de los archivos?
  • ¿Los componentes son pequeños, legibles y comprobables?
  • ¿Existe alguna convención compartida por el equipo?

Conclusión

Organizar bien un proyecto Angular no significa crear una estructura complicada. Significa crear una estructura que sea clara, predecible y adecuada para el crecimiento. La arquitectura basada en funciones le ayuda a pensar en la funcionalidad del mundo real. La separación entre núcleo y compartido evita confusiones. Los componentes independientes hacen que las dependencias sean más explícitas. Las señales simplifican la gestión estatal. El patrón de componentes inteligentes versus tontos mejora la legibilidad, la reutilización y la capacidad de prueba.

Lo más importante es no esperar hasta que el proyecto crezca para pensar en arquitectura. Las decisiones que se toman al principio influyen en todo el ciclo de vida de la aplicación. Una estructura limpia le permite agregar nuevas páginas, nuevas funciones y nuevos desarrolladores sin convertir el código en un laberinto.

Si estás creando una aplicación Angular profesional, comienza con una regla simple: todo debe tener un lugar específico. Las características deben contener la lógica del producto, el núcleo debe contener la infraestructura global y lo compartido debe contener elementos verdaderamente reutilizables. A partir de ahí, agregue componentes independientes, carga diferida, señales y una separación clara entre componentes inteligentes y tontos.

Una arquitectura escalable no es la más compleja, sino la que el equipo puede comprender, mantener y evolucionar con el tiempo. Angular ofrece todas las herramientas necesarias: depende de nosotros usarlas con disciplina, coherencia y sentido común.

💬 Notas de los lectores

0 notas

Escribe una nota

Comparte tu opinión, una sugerencia o un cumplido

Últimas notas

Aún no hay notas. ¡Sé el primero en comentar!