Angular te permite desarrollar muy rápido y, precisamente por eso, es fácil crear un caos enorme sin darte cuenta. Si tu app ya pasó de unas 30 pantallas, probablemente ya lo hayas sentido: change detection ejecutándose sin necesidad, un subscribe olvidado reteniendo memoria y formularios con un any disfrazado. Las buenas prácticas que se presentan a continuación no son reglas de estilo; son lo que separa un proyecto que escala de uno que se bloquea con cada nueva feature.

Standalone es el estándar ahora: deja de arrastrar NgModule
Desde Angular 15, los componentes standalone son estables y, a partir de Angular 17, son el valor predeterminado del CLI. Si todavía creas un @NgModule para cada feature, estás pagando un impuesto innecesario de boilerplate. Standalone declara las dependencias directamente en el componente, en imports: [], y se acaba el ir y venir de declarar y exportar elementos en módulos.
La ganancia real aparece en el lazy loading y el tree-shaking: sin módulos intermedios, el bundler puede identificar mejor qué se puede eliminar. Migra de forma gradual; el propio Angular tiene un schematic (ng generate @angular/core:standalone) para hacer la conversión por etapas. No necesitas reescribirlo todo en un fin de semana.
Componente tonto, componente inteligente: divídelos o pagarás después
Es una idea antigua, pero se sigue ignorando. El componente inteligente (container) sabe de dónde vienen los datos: inyecta services, se comunica con el store y ejecuta side effects. El componente tonto (presentational) solo recibe datos mediante @Input y emite eventos mediante @Output; no sabe si los datos vienen de una API, de una caché o de un mock.
¿Por qué importa esto en la práctica? Un componente tonto es trivial de probar —entra un input y sale un output— y se puede reutilizar fácilmente. Cuando mezclas la obtención de datos con la renderización en el mismo lugar, cada test se convierte en un mock de HttpClient y cada reutilización termina en copiar y pegar. La regla sencilla es esta: si el componente tiene un constructor lleno de services y un template grande, probablemente quiera ser dos componentes.
OnPush + signals: change detection que no te traiciona
De forma predeterminada, Angular vuelve a comprobar todo el árbol con cada evento. En una app pequeña nadie lo nota; en una app grande puede provocar lag al escribir. ChangeDetectionStrategy.OnPush le indica a Angular que solo vuelva a renderizar cuando un @Input cambia por referencia, cuando se dispara un evento en el componente o cuando un observable emite mediante el async pipe.
Adopta OnPush como estándar, no como una optimización de último recurso. Y aquí los signals —estables desde Angular 17— encajan perfectamente: un signal() notifica al change detection de forma granular, sin depender de la inmutabilidad manual. computed() deriva el estado sin recalcular innecesariamente. La combinación de OnPush + signals es hoy el camino más predecible: menos markForCheck() disperso y menos bugs de pantallas que no se actualizan.
RxJS sin fugas: async pipe primero, unsubscribe de forma consciente
Los subscribes manuales son la mayor fuente de memory leaks en Angular. La regla número uno es dejar que el async pipe se encargue. Se suscribe en el template y cancela la suscripción automáticamente cuando el componente se destruye. *ngIf="data$ | async as data" resuelve el 80 % de los casos sin una sola línea de unsubscribe.
Cuando necesites suscribirte en TypeScript —por ejemplo, para un side effect o un tap que abre un modal— no lo dejes suelto. Usa takeUntilDestroyed() (de @angular/core/rxjs-interop, estable desde Angular 16), que vincula la suscripción al ciclo de vida del componente sin boilerplate de ngOnDestroy. Evita el .subscribe() aislado sin cleanup: parece funcionar, pero filtra memoria silenciosamente durante semanas, hasta que la app se vuelve pesada sin una explicación clara.
Forms tipados y rutas lazy: qué cambia realmente
Desde Angular 14, los reactive forms están tipados. Si todavía obtienes form.value y recibes un any, estás desperdiciando uno de los mayores avances de DX de los últimos años. Decláralos con FormGroup<{ nombre: FormControl<string> }> o deja que FormBuilder infiera los tipos; el compilador te avisará cuando accedas a un campo inexistente o pases un tipo incorrecto. Los formularios son uno de los lugares donde más duelen los bugs de tipos; tiparlos ofrece un retorno inmediato.
En cuanto a las rutas, carga todo lo que no sea la pantalla inicial mediante loadComponent / loadChildren. Con standalone, loadComponent: () => import('./pagina').then(m => m.Pagina) ya divide el bundle sin complicaciones. El usuario descarga solo lo que necesita la ruta actual y el first load lo agradece.
Estructura de carpetas por feature, no por tipo
Una carpeta components/, otra services/ y otra models/, cada una con cincuenta archivos, es el clásico que no escala. Terminas saltando de una carpeta a otra para entender una sola feature. Organiza por dominio: features/pedidos/, con el componente, el service, los tipos y el test juntos. Usa shared/ solo para lo que sea realmente reutilizable y core/ para los singletons de la app, como guards e interceptors.
La prueba es sencilla: para eliminar una feature, deberías poder borrar una carpeta. Si necesitas buscar archivos en seis lugares distintos, tu estructura está organizada por tipo y cada cambio costará más de lo necesario.
Ninguna de estas prácticas es una revolución; son decisiones que tomas una vez y aprovechas durante años. Empieza por lo que más duele hoy en tu proyecto: OnPush si tienes lag, async pipe si tienes fugas de memoria o feature folders si te pierdes al buscar las cosas. Después, adopta el resto como estándar. Un código Angular sostenible no depende de un framework nuevo, sino de dejar de posponer estas decisiones.

