
Desde Angular 16 en adelante, el framework se fue de SPA y ha vuelto renovado: cada versión da pasitos (a veces zancadas) hacia un estilo más funcional y declarativo, pero sin soltar del todo sus orígenes de programación orientada a objetos. En este artículo te cuento las novedades más jugosas y cómo el Injection Context se ha convertido en uno de los protagonistas de esta nueva etapa.
Disclaimer: Este artículo ha sido creado con el apoyo de IA, pero mantiene autonomía total del redactor. Los chistes más graciosos, cuidadosamente elegidos, así como la estructura y los puntos clave, fueron seleccionados para que aprendieras, rieras y no te quedaras dormid@ en el intento.
Algunos conceptos teóricos
En desarrollo SW, 2 paradigmas mandan: la Programación Orientada a Objetos (POO) y la Programación Funcional (PF). Podría simplemente soltar aquí la chapa teórica, pero no quiero que esto parezca un manual de universidad. Así que, como soy majo, voy a meter también ejemplos prácticos.
- POO organiza el código en torno a objetos, que combinan datos y comportamientos. Sus principios clave incluyen:
- Encapsulamiento: protege el estado interno del objeto, exponiendo solo lo necesario.
- Herencia: permite compartir y extender comportamientos entre clases.
- Polimorfismo: facilita tratar distintos objetos de manera uniforme mediante interfaces comunes.
- PF, en cambio, organiza el código en torno a funciones. Sus pilares son:
- Funciones puras: no producen efectos secundarios y no tienen dependencias externas.
- Immutabilidad: modificaciones de los datos (estado) implican la creación de nuevos objetos, sin alterar el original.
- Composición: combina funciones simples para crear comportamientos más complejos.
- Funciones de primera clase: funciones pueden almacenarse en variables, pasarse como argumentos y devolverse desde otras funciones.
Desafío: ¿Puedes adivinar cuál de estas implementaciones es POO y cuál es PF… antes de que tu café se enfríe?

Solución:
Implementación de la izquierda (POO):
- Encapsulamiento:
CodingBooksServiceengloba y protege su propio estado, ya que mantiene datos internos y métodos que mutan (modifican) ese estado. TantobookscomogetBookTitleson privados, lo que asegura que los datos internos no se pueden modificar desde fuera de la clase. - Herencia y polimorfismo:
CodingBooksServiceextiendeBooksService, lo que permite compartir y reutilizar comportamientos de la clase base, así como modificar o ampliar funcionalidades. Gracias a esto, podríamos tratar varias subclases deBooksServicede manera uniforme, aplicando polimorfismo. - Estado: la función
getBookTitledepende del estadothis.booksy de la funcióngetNameById, cuyos valores y comportamiento podrían ser modificados. Por ello, no es una función pura, ya que no garantiza siempre el mismo output para los mismos argumentos. Además,getBookTitlemodificapreviousTitleen lugar de devolver un nuevo resultado, por lo que no es immutable.
Implementación de la derecha (PF):
- Funciones puras:
getBookTitlerecibe sus datos y devuelve un resultado sin tocar nada externo, esta función podría ser reutilizada - Immutabilidad:
booksno se modifica; solo se consulta para generar nuevo un valor. - Composición y funciones de 1a clase:
getBookTitlese puede pasar a otras funciones, combinar o reutilizar fácilmentegetBookTitle(getCodingBooks()). - Sin estado interno: cada llamada es predecible y no depende de ningún objeto complicado.
Si piensas que tienes que elegir un solo paradigma y empezar a odiar al otro, vas a estar perdiendo más tiempo que debugueando un error undefined is not a function a las 3 de la mañana.
La línea que separa OOP y PF no es un muro de hormigón, es más bien como una raya de tiza que se borra sola. Rara vez la solución consiste en elegir un bando y quedarse ahí de por vida.
Lo más habitual es que cada problema tenga su solución más adecuada. La magia está en entender los pros y contras de cada enfoque y ser capaz de mezclarlos de manera armoniosa.
Las nuevas funciones de angular
0. bootstrapApplication()
1. makeEnvironmentProviders() / importProvidersFrom()
2. provideHttpClient() / provideRouter() / provideAppInitializer()
3. inject() / runInInjectionContext()
4. viewChild() / viewChildren()
5. contentChild() / contentChildren()
6. input() / model() / output()
7. signal() / linkedSignal() / computed() / effect()
8. toSignal() / toObservable()
9. afterEveryRender() / afterNextRender() / afterRenderEffect()
10. resource() / httpResource() / rxResource()
11. CanActivateFn / ResolveFn / HttpInteceptorFn
(... muchas otras)
Bootstrap y inyección de dependencias
Antes de bootstrapApplication(), Angular arrancaba aplicaciones usando un módulo raíz acoplador (AppModule) que esencialmente encapsulaba AppComponent, otros NgModule() y los providers, un enfoque claramente orientado a objetos.
Con bootstrapApplication(), ahora se puede iniciar la app directamente desde AppComponent standalone, lo que apunta a un estilo más funcional y declarativo, ya que las dependencias del componente son establecidas como importaciones en su decorador @Component , permitiendo una mayor reutilización.
Aunque en Angular los componentes siguen siendo clases (objetos), muchos en la comunidad (🙄) advocan por un formato de autoría con componentes funcionales al estilo React.
En este nuevo ecosistema entran en juego 2 funciones auxiliares que gestionan la configuración de dependencias y la transición desde el modelo basado en NgModule hacia el standalone.
makeEnvironmentProviders()define la inyección de dependencias de proveedores a nivel de aplicación (environment injector), este tipo de proveedores solo se pueden inyectar enbootstrapApplication()o en rutas y nunca dentro de un componente o directiva (i.e.HttpClient).importProvidersFrom()sirve principalmente para migrar importaciones de clasesNgModulea proveedores funcionales de nivel de aplicación. Es una especie de puente que permite aprovechar módulos existentes dentro del nuevo enfoque standalone, sin necesidad de reescribirlos desde cero.

El error a derecha se debe a que se está poniendo un tipo de proveedor que solo funciona a nivel global (EnvironmentProviders) dentro de un componente: solo acepta proveedores normales (Provider).

Proveedores a través de funciones
Las funciones provideHttpClient() y provideRouter() es la versión más funcional y directa de proveer algunas de las dependencias más comunes, sin necesidad de declararlas en un NgModule especifico. Además, si tu CV pone "arquitecto", debes saber que estas funciones se pueden componer con otras funciones como withInterceptors(), withComponentInputBinding() y withRouterConfig().

Los ejemplos muestran cómo se reemplazan las configuraciones de módulos y proveedores por funciones composibles: los interceptores de HTTP se definen como funciones (HttpInterceptorFn), el router se configura con helpers (withRouterConfig, withComponentInputBinding) y los valores globales como APP_CONFIG se registran mediante makeEnvironmentProviders. Además, importProvidersFrom permite importar módulos existentes (como TranslocoModule) en el formato de proveedor. Esto simplifica la estructura de arranque y centraliza la configuración.
El contexto de inyección
En el universo mágico de Angular, el injection context es simplemente el lugar donde la aplicación busca las dependencias que un componente o servicio necesita. Cada componente o módulo tiene su propio inyector, y todos juntos forman un gran árbol de inyección. En la raíz de ese árbol están los EnvironmentProviders declarados en bootstrapApplication, accesibles desde cualquier parte de la aplicación.
Por eso, algunas funciones de Angular solo pueden usarse dentro del universo Angular, si no, lanzan un error avisando que la magia no funciona en el mundo real.

Los contextos de inyección son:
- Constructores de un componente o proveedor o servicio;
- inicialización de las propriedades de un componente o servicio;
- funciones factoría de un proveedor o servicio;
- Función argumento de la función
runInInjectionContext.
Desafío:
La versión siguiente no presenta errores de compilación ni de ejecución! Sabrías indicar el motivo antes de fichar salida jejejeje?

Solución:
Normalmente, cuando intentas usar inject() fuera de un contexto válido (constructor, provider, factory, etc.), Angular lanza error:NG0203: ... must be called from ... injection context
Peeero en este ejemplo no pasa eso, porque la función todosResource() no se ejecuta en el “aire”, sino durante la inicialización de la propiedad resource del componente App. La inicialización de propiedades de un componente forma parte de los contextos de inyección permitidos en Angular. Así que cuando Angular crea la instancia del componente App, entra en un contexto de DI válido, inicializa resource = todosResource(), y en ese momento inject(HttpClient) tiene acceso al inyector que construye el componente. Esto permite reutilizar la función todosResource() en múltiplos componentes.
La función inject es probablemente la más interesante para entender lo que distingue a Angular de otros frameworks. Permiten aprovechar todo el poder de la DI.
Aquí te dejo algunos ejemplos de lo que puedes alcanzar con esa herramienta.
Escenario 1: Tienes un dashboard de detalles en los que habitualmente tienes que confirmar ciertos datos que recibes como input de un tipo genérico y deseas notificar con alertas y enseñar un toast y generar un log.

Al usar inject en la clase base (BaseDetails<T>), se consigue centralizar la DI comunes (config, servicios de modal, toast y logging) sin repetir código en cada componente hijo. Esta arquitectura combina la herencia de la POO, donde los componentes concretos (DocumentsDetails) extienden de una clase genérica y heredan sus capacidades, con un enfoque más funcional y declarativo, ya que inject es una función pura que resuelve las dependencias. El resultado es una solución que promociona reutilización, limpieza de código y legibilidad.
Escenario 2: Quieres solicitar múltiplos datos asíncronos en paralelo, aplicar estrategias de reintento en casos de error, encadenar peticiones, todo esto en una función casi-pura reutilizable, integrada con las funciones resource de modo a usar Signals para desencadenar las peticiones y obtener el estado de carga y error.

- Paralelo →
forkJoinejecuta varias peticiones HTTP al mismo tiempo. - Reintentos →
retry(2)vuelve a intentar la petición inicial en caso de error. - Encadenamiento →
switchMapusa el resultado de la primera petición para lanzar las siguientes. - Reutilizable →
fetchDataes una función externa, sin estado propio, fácil de usar en otros lugares. - Signals → el Signal
filterdispara automáticamente las peticiones al cambiar. - rxResource → orquesta la integración Signal ↔ RxJS: manejo de
isLoading/value/error, suscripción/limpieza automática.
Escenario 3: La app necesita controlar acceso por permisos obtenidos desde una API, antes de navegar, las vistas deben ser protegidas por los permisos del usuario.

- InjectionToken (
PERMISSIONS): Define unWritableSignal<string[]>inicializado vacío a través de una función factoría. Provee un estado reactivo e inyectable en toda la app.
- Resolver (
resolvePermissions): una funcion factoria que inyecta el SignalPERMISSIONS, llama al backend conHttpClientusando eluserId, actualiza el Signal con los permisos recibidos (tap(permissions.set)). la dependenciapermissionsno se inyecta in-line (tap(inject(PERMISSIONS).set)) porque estaría fuera del contexto de inyección cuando se reciben los permisos del backend.
- Guard (
hasPermission): una función factoría de orden superior que recibe un permiso y devuelve un guard.
Escenario 4: Un componente tabla reutilizable necesita un FiltersService, pero su comportamiento depende de si ya existe uno definido en un ancestro (padre). Si el padre ya tiene un FiltersService, el componente debe reutilizarlo (heredar filtros compartidos). Si no existe en la jerarquía, el componente debe crear uno propio (para tener filtros independientes).

inject(FiltersService, { optional: true, skipSelf: true }): Busca unFiltersServicesolo en ancestros (por esoskipSelf: true), si no lo encuentra, devuelvenullgracias aoptional: true.- Si hay un
parentFilters, lo reutiliza, si no, crea un nuevoFiltersServiceconnew FiltersService().
Conclusión
Angular está abrazando cada vez más la programación funcional, pero sin renegar de sus raíces orientadas a objetos. El resultado es un framework híbrido, más flexible y declarativo, que permite combinar lo mejor de ambos mundos según lo que cada escenario necesite.
No se trata de elegir un único paradigma, sino de entender cómo y cuándo aplicarlos. Las nuevas funciones —desde bootstrapApplication() hasta inject() y los signals— apuntan a una forma de trabajar más limpia, componible y predecible, que además reduce el ruido de configuración.
En definitiva, Angular 16+ no es un cambio radical, sino una evolución natural: menos acoplamiento, más expresividad y un guiño constante hacia un estilo más funcional, sin perder la robustez que siempre lo caracterizó.