Lección 3 de 412 min
Servicios e inyección de dependencias
Servicios, inject(), proveedores, la jerarquía de inyectores, InjectionToken y HttpClient.
La cabecera de la tienda muestra cuántos productos hay en el carrito; la página de producto tiene un botón para añadir uno más; el resumen de compra lista todo lo elegido. Tres componentes distintos que trabajan con el mismo carrito. ¿Dónde vive ese carrito y cómo llega a cada uno?
La respuesta en Angular son los servicios y la inyección de dependencias: clases que guardan la lógica y los datos compartidos, y un mecanismo que las entrega a quien las pida. En esta lección verás cómo se declaran, cómo se configuran los proveedores, cómo busca Angular cada dependencia y cómo se habla con el servidor con HttpClient.
Servicios e inyección de dependencias
Inyección de dependencias
Cada pieza pide lo que necesita y otro se encarga de dárselo ya construido.
- Siglas:
- DI (Dependency Injection)
Una dependencia es algo que una clase necesita para trabajar: un componente que muestra productos necesita quien se los traiga del servidor. Podría crearlo él mismo con new, pero quedaría atado a esa implementación concreta y sería difícil de probar y de compartir.
Con la inyección de dependencias (DI), la clase solo declara qué necesita y un inyector se lo entrega. El inyector sabe cómo crear cada cosa gracias a los proveedores y guarda las instancias para reutilizarlas. Es la inversión de control que viste en la primera lección, aplicada a las dependencias.
Úsalo cuando
Para todo lo que no es interfaz: acceso a datos, estado compartido, configuración, registro de errores. Los componentes piden servicios; no los construyen.
Ten en cuenta
No es magia: si nadie ha registrado un proveedor para lo que pides, Angular lanza un NullInjectorError («No provider for…») al crear la pieza que lo pide.
// Sin DI: el componente crea y configura su dependencia
export class ListaProductos {
private api = new ProductosApi(/* ¿y qué le paso aquí? */);
}
// Con DI: solo la pide; el inyector se la entrega construida
export class ListaProductos {
private api = inject(ProductosApi);
}Servicios
Clases sin plantilla para la lógica y los datos que comparten varios componentes.
- En root:
- Una instancia (singleton)
- Generar:
- ng g service
Un servicio es una clase normal que agrupa una responsabilidad: hablar con la API de productos, guardar el carrito, saber quién ha iniciado sesión. Con @Injectable({ providedIn: 'root' }) le pides a Angular que lo registre en el inyector raíz: habrá una sola instancia para toda la aplicación, creada la primera vez que alguien la pida.
Además, providedIn: 'root' permite el tree-shaking: si ningún código usa el servicio, el bundler lo deja fuera del paquete final. Las versiones más recientes añaden @Service(), una forma abreviada de lo mismo que exige pedir las dependencias con inject().
Úsalo cuando
Para la lógica que no es de presentación o que comparten varios componentes. Un componente debería limitarse a mostrar datos y reaccionar al usuario.
Ten en cuenta
Un servicio en root vive mientras dure la app y su estado se comparte: si guardas ahí los datos de un formulario, el siguiente componente que lo use los verá. Para estado propio de cada componente, mira la jerarquía de inyectores.
@Injectable({ providedIn: "root" })
export class Carrito {
private lineas = signal<Linea[]>([]);
readonly total = computed(() =>
this.lineas().reduce((suma, l) => suma + l.precio * l.unidades, 0),
);
anadir(producto: Producto) {
this.lineas.update((ls) => [...ls, { ...producto, unidades: 1 }]);
}
}inject() frente al constructor
Dos formas de pedir una dependencia; inject() es la recomendada hoy.
La forma clásica es declarar la dependencia como parámetro del constructor: constructor(private carrito: Carrito) {}. Angular lee el tipo y te pasa la instancia. La función inject(Carrito) hace lo mismo, pero se usa al inicializar un campo, lo que deja el código más corto.
inject() solo funciona dentro de un contexto de inyección: mientras Angular crea un componente, servicio, directiva o pipe (en sus campos o en su constructor) y en las funciones que Angular ejecuta con ese contexto, como guards, resolvers e interceptores. Por eso esas piezas pueden ser simples funciones en lugar de clases.
Úsalo cuando
En todo código nuevo. Funciona igual en clases y en funciones, y simplifica la herencia: no tienes que repetir los parámetros del constructor en cada subclase.
Ten en cuenta
Fuera de un contexto de inyección, por ejemplo dentro de un setTimeout o después de un await, inject() lanza un error. Guarda lo que necesites en un campo al crear la clase y úsalo después.
// Con el constructor (estilo clásico)
export class Cabecera {
constructor(private carrito: Carrito, private router: Router) {}
}
// Con inject() (recomendado)
export class Cabecera {
private carrito = inject(Carrito);
private router = inject(Router);
total = this.carrito.total;
}Proveedores: las recetas del inyector
Un proveedor le dice al inyector qué entregar cuando alguien pide un token, que normalmente es una clase. Se escribe { provide: Token, useX: … } y se registra en un array providers: el de app.config.ts (para toda la app), el de una ruta (para esa sección) o el de un componente (para él y sus hijos). providedIn: 'root' es un atajo para el caso más común.
useClass
Entrega una instancia de otra clase cuando alguien pide un token.
{ provide: Registro, useClass: RegistroEnConsola } significa «cuando alguien pida Registro, crea un RegistroEnConsola». El código que inyecta Registro no cambia; solo cambia la implementación que recibe. Escribir solo la clase, providers: [Carrito], es la abreviatura de { provide: Carrito, useClass: Carrito }.
Úsalo cuando
Para cambiar una implementación por otra con la misma forma: una versión falsa en las pruebas, otra distinta según el entorno.
Ten en cuenta
Si registras la misma clase en dos sitios obtienes dos instancias distintas, cada una con su propio estado.
export abstract class Registro {
abstract error(mensaje: string): void;
}
export class RegistroEnConsola extends Registro {
error(mensaje: string) {
console.error(mensaje);
}
}
// app.config.ts
providers: [{ provide: Registro, useClass: RegistroEnConsola }]useValue
Entrega un valor ya hecho: un objeto de configuración, un texto, un doble de pruebas.
{ provide: API_URL, useValue: 'https://api.ejemplo.com' } entrega exactamente ese valor, sin construir nada. Es ideal para datos que ya conoces al arrancar, como la URL de la API o las opciones de una librería. Como esos valores no son clases, suelen ir acompañados de un InjectionToken, que verás en el siguiente grupo.
Úsalo cuando
Para configuración estática y para sustituir un servicio por un objeto simulado en las pruebas.
Ten en cuenta
El valor se comparte tal cual: si es un objeto y alguien lo modifica, todos ven el cambio. Congélalo con Object.freeze si no debe cambiar.
providers: [
{ provide: API_URL, useValue: "https://api.ejemplo.com" },
// En un test: un carrito falso con lo mínimo
{ provide: Carrito, useValue: { total: signal(0), anadir: () => {} } },
]useFactory
Entrega lo que devuelva una función, que decide en el momento cómo construirlo.
{ provide: Almacen, useFactory: () => … } ejecuta la función la primera vez que alguien pide el token y entrega lo que devuelve. Dentro puedes usar inject() para leer otras dependencias y decidir qué construir según la configuración, el entorno o la plataforma.
Úsalo cuando
Cuando la creación depende de algo que solo se sabe al ejecutar: elegir entre dos implementaciones, pasar parámetros a un constructor o envolver una librería que no es de Angular.
Ten en cuenta
La función se ejecuta una vez por inyector, no cada vez que alguien inyecta el token. Si necesitas un objeto nuevo en cada uso, haz que el token entregue una función que lo cree.
providers: [
{
provide: Almacen,
useFactory: () => {
const plataforma = inject(PLATFORM_ID);
// En el servidor no existe localStorage
return isPlatformBrowser(plataforma)
? new AlmacenLocal(localStorage)
: new AlmacenEnMemoria();
},
},
]useExisting
Convierte un token en alias de otro: los dos entregan la misma instancia.
{ provide: RegistroAntiguo, useExisting: Registro } no crea nada: cuando alguien pide RegistroAntiguo, el inyector busca Registro y entrega esa misma instancia. Es la diferencia con useClass, que crearía una segunda instancia con su propio estado.
Úsalo cuando
Para migrar poco a poco de un servicio viejo a uno nuevo sin tocar todos los componentes, o para exponer un servicio grande a través de una clase abstracta más pequeña.
Ten en cuenta
Si el token de destino no tiene proveedor, el alias tampoco funciona. Y evita encadenar alias: cuesta seguirlos.
providers: [
Registro,
// Los componentes antiguos piden RegistroAntiguo
// y reciben la misma instancia que los nuevos
{ provide: RegistroAntiguo, useExisting: Registro },
]Inyectores y tokens
La jerarquía de inyectores
Angular busca cada dependencia de abajo arriba: primero en los elementos, luego en el entorno.
No hay un único inyector, sino un árbol. Los inyectores de entorno (environment injectors) contienen los proveedores de la aplicación: el raíz, con lo de providedIn: 'root' y app.config.ts, y otros por debajo, como los de las rutas que declaran providers. Los inyectores de elemento (element injectors) se crean para los componentes y directivas que declaran providers en su decorador.
Cuando un componente pide algo, Angular sube por los inyectores de elemento, desde él hasta la raíz del árbol de componentes. Si no lo encuentra, sigue por los inyectores de entorno. El primero que tenga un proveedor gana; si ninguno lo tiene, lanza NullInjectorError.
Úsalo cuando
Pon un servicio en los providers de un componente cuando cada instancia necesite su propia copia: por ejemplo, el estado de un asistente de varios pasos que aparece dos veces en la misma página.
Ten en cuenta
Ese servicio se crea con el componente, lo comparten sus hijos y se destruye con él. Opciones como inject(X, { optional: true }) o { skipSelf: true } cambian cómo se busca.
@Component({
selector: "app-asistente",
// Cada <app-asistente> tiene su propio EstadoAsistente,
// compartido con los pasos que contiene
providers: [EstadoAsistente],
template: `<app-paso-datos /> <app-paso-pago />`,
})
export class Asistente {}
export class PasoPago {
// Sube por el árbol y encuentra el del Asistente que lo contiene
private estado = inject(EstadoAsistente);
}InjectionToken
Un identificador para inyectar lo que no es una clase: textos, objetos, funciones.
Una clase sirve a la vez de token y de tipo, pero un texto o una interfaz de TypeScript no existen al ejecutar. Para eso está InjectionToken: new InjectionToken<string>('API_URL') crea un identificador único y tipado. El texto es solo una descripción para los mensajes de error.
Puedes darle un valor por defecto con factory; así funciona sin registrarlo en ningún sitio, igual que un servicio con providedIn: 'root'. Si quieres otro valor, lo sobrescribes con useValue en providers.
Úsalo cuando
Para configuración (la URL de la API, las opciones de una librería), para objetos del entorno como window y para inyectar funciones.
Ten en cuenta
Dos InjectionToken con la misma descripción son tokens distintos: exporta la constante y reutilízala en lugar de crear otra.
export const API_URL = new InjectionToken<string>("API_URL", {
providedIn: "root",
factory: () => "/api",
});
// app.config.ts: otro valor para producción
providers: [{ provide: API_URL, useValue: "https://api.ejemplo.com" }]
// En un servicio: TypeScript sabe que es un string
private url = inject(API_URL);Nota
Esta jerarquía es la razón de que las pruebas en Angular sean cómodas: en un test configuras un inyector con useValue o useClass para las dependencias y el componente recibe tus dobles sin cambiar una línea. Lo verás en el módulo de testing.
Hablar con el servidor: HttpClient
HttpClient
El cliente HTTP de Angular: peticiones tipadas que devuelven Observables.
- Devuelve:
- Observable
HttpClient hace peticiones al servidor con métodos como get, post, put y delete. Le indicas el tipo de la respuesta (get<Producto[]>(…)), convierte el JSON automáticamente y devuelve un Observable, que verás a fondo en el módulo de gestión de estado. En las versiones recientes usa fetch por debajo.
Según la documentación actual, desde la v21 puedes inyectar HttpClient sin configurar nada; en versiones anteriores había que añadir provideHttpClient() en app.config.ts. Esa función sigue siendo el sitio para ajustar su comportamiento, por ejemplo para añadir interceptores con withInterceptors().
Úsalo cuando
Para cualquier comunicación con tu API desde Angular. Encapsúlalo en un servicio (ProductosApi) en lugar de llamarlo desde cada componente.
Ten en cuenta
El Observable es «perezoso»: si nadie se suscribe (con subscribe(), el pipe async o toSignal()), la petición no se envía. Y cada suscripción lanza una petición nueva.
@Injectable({ providedIn: "root" })
export class ProductosApi {
private http = inject(HttpClient);
private url = inject(API_URL);
listar() {
return this.http.get<Producto[]>(`${this.url}/productos`);
}
crear(nuevo: NuevoProducto) {
return this.http.post<Producto>(`${this.url}/productos`, nuevo);
}
}Interceptores funcionales
Funciones que pasan por todas las peticiones: añadir cabeceras, registrar, tratar errores.
Un interceptor se coloca entre tu código y la red. Recibe cada petición y una función next para pasarla al siguiente eslabón; puede modificarla antes de enviarla y transformar la respuesta a la vuelta. Como las peticiones son inmutables, para cambiar una la clonas con req.clone().
En Angular moderno, un interceptor es una función del tipo HttpInterceptorFn y se registra con provideHttpClient(withInterceptors([…])). Se ejecutan en el orden del array y dentro puedes usar inject() para leer servicios, como el que guarda el token de sesión.
Úsalo cuando
Para lo que afecta a todas las peticiones: añadir el token de autenticación, mostrar un indicador de carga global, reintentar ante fallos de red o mandar al login cuando llega un 401.
Ten en cuenta
No envíes el token a otros dominios: comprueba la URL antes de añadirlo. Los interceptores basados en clases (HttpInterceptor) siguen existiendo, pero la documentación recomienda los funcionales porque su orden es más predecible.
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = inject(Sesion).token();
if (!token || !req.url.startsWith("/api")) return next(req);
const conToken = req.clone({
setHeaders: { Authorization: `Bearer ${token}` },
});
return next(conToken);
};
// app.config.ts
providers: [provideHttpClient(withInterceptors([authInterceptor]))]Interactivo
Las cuatro recetas de un proveedor
Activa o desactiva opciones para compararlas cara a cara.
| Característica | useClass | useValue | useFactory |
|---|---|---|---|
| Qué entrega | Una instancia de la clase indicada | El valor tal cual | Lo que devuelva la función |
| ¿Crea algo? | Sí, con new | No | Lo que decida la función |
| Ejemplo | { provide: Registro, useClass: RegistroEnConsola } | { provide: API_URL, useValue: '/api' } | { provide: Almacen, useFactory: () => … } |
| Úsalo para | Cambiar de implementación | Configuración y dobles de prueba | Decidir en tiempo de ejecución |
Ponlo en práctica
Saber qué es cada cosa es la mitad del trabajo. La otra mitad es reconocer cuándo usarla.
Interactivo
¿Qué usarías?
Lee cada situación y elige la opción que mejor la resuelve. Fallar aquí no cuenta: es para practicar.
En un test quieres que el componente reciba un ProductosApi falso que devuelve siempre los mismos tres productos.
Para recordar
- Con la inyección de dependencias, cada clase pide lo que necesita y el inyector se lo entrega ya construido.
- Un servicio con
providedIn: 'root'es una instancia única para toda la app; pídelo coninject(). - Un proveedor es una receta:
useClasscrea,useValueentrega,useFactorydecide yuseExistingreutiliza. - Angular busca primero en los inyectores de elemento (los componentes) y después en los de entorno (las rutas y la raíz).
HttpClientdevuelve Observables perezosos, y los interceptores funcionales tratan todas las peticiones en un solo sitio.
Comprueba lo aprendido
Cuestionario
Responde las 5 preguntas para completar la lección.
1.¿Qué consigues con
@Injectable({ providedIn: 'root' })?2.¿En cuál de estos sitios lanzará un error
inject(Carrito)?3.Registras
{ provide: B, useExisting: A }. ¿Qué recibe quien inyectaB?4.Un componente declara
providers: [Filtros]y uno de sus hijos haceinject(Filtros). ¿Qué instancia recibe el hijo?5.Llamas a
this.http.get('/api/productos')en un servicio, pero en la pestaña Red del navegador no aparece ninguna petición. ¿Por qué?