Application demo: Joe’s Robot Shop (robot parts e-commerce)
Table of Contents
- Course Overview
- Creating Custom Angular Services
- Reactive and Asynchronous Services
- Understanding Angular Dependency Injection
- Angular Service Providers
- Angular Service Injectors
- Architecture Diagrams
- Reference Tables
- Best Practices and Summary
1. Course Overview
This course covers the fundamentals and advanced topics related to Angular services and the Dependency Injection (DI) system. The main themes:
- Creating and using Angular services
- Managing reactivity with RxJS and Signals
- Asynchronous services (HTTP)
- Angular DI architecture
- Providers and Injectors: impact on service instantiation
Prerequisites: Solid understanding of Angular fundamentals (components, modules or standalone, basic routing).
2. Creating Custom Angular Services
Why Are Services Necessary?
An Angular service is simply a TypeScript class used to execute actions or store data shared between multiple parts of the application.
Problem without a service: a component takes on too many responsibilities.
// ❌ Bad practice: business logic inside the component
@Component({ ... })
export class CatalogComponent implements OnInit {
products: Product[] = [];
ngOnInit() {
// The component knows the API URL, handles JSON conversion, etc.
fetch('/api/products')
.then(res => res.json())
.then(data => this.products = data);
}
addToCart(product: Product) {
// The component also manages the cart...
this.cart.push(product);
}
}
Solution with a service: each class has a single responsibility.
// ✅ Good practice: delegation to services
@Component({ ... })
export class CatalogComponent {
constructor(
private productsService: ProductsService,
private cartService: CartService
) {}
ngOnInit() {
this.productsService.getProducts().subscribe(data => this.products = data);
}
addToCart(product: Product) {
this.cartService.add(product);
}
}
The Single Responsibility Principle (SRP)
Single Responsibility Principle: every class should have only one reason to change.
| Component | Its responsibility |
|---|---|
CatalogComponent | Display products, manage user interactions |
ProductsService | Retrieve product data from the API |
CartService | Manage cart state |
Creating an Angular Service
The Angular CLI generates the skeleton: ng generate service catalog/products
// src/app/catalog/products.service.ts
import { Injectable } from '@angular/core';
import { Product } from './product.model';
import { productsArray } from './products-data';
@Injectable({ providedIn: 'root' })
export class ProductsService {
getProducts(): Product[] {
return productsArray;
}
}
The @Injectable decorator tells Angular that this class can be injected as a dependency. providedIn: 'root' means the service is available throughout the application (singleton at the root level).
Injecting and Using a Service
Injection via the constructor (TypeScript shorthand syntax):
@Component({
selector: 'bot-catalog',
templateUrl: './catalog.component.html',
})
export class CatalogComponent {
products: Product[];
// Angular automatically injects an instance of ProductsService
constructor(private productsService: ProductsService) {
this.products = this.productsService.getProducts();
}
}
The private productsService: ProductsService syntax in the constructor is equivalent to:
private productsService: ProductsService;
constructor(productsService: ProductsService) {
this.productsService = productsService;
}
Injection via the inject() function (Angular 14+, modern approach):
import { inject } from '@angular/core';
@Component({ ... })
export class CatalogComponent {
private productsService = inject(ProductsService);
private cartService = inject(CartService);
}
Inter-Component Communication with Services
Services enable communication between components that are not in a direct parent-child relationship (no @Input/@Output).
Example: CatalogComponent, SearchComponent, and CartComponent share the same CartService.
// src/app/core/cart.service.ts (initial simple version)
import { Injectable } from '@angular/core';
import { Product } from '@shared/product.model';
@Injectable({ providedIn: 'root' })
export class CartService {
cart: Product[] = [];
add(product: Product) {
this.cart.push(product);
}
remove(product: Product) {
this.cart = this.cart.filter(p => p !== product);
}
get cartTotal() {
return this.cart.reduce((prev, next) => {
let discount = next.discount && next.discount > 0 ? 1 - next.discount : 1;
return prev + next.price * discount;
}, 0);
}
}
3. Reactive and Asynchronous Services
The Need for Reactivity
Reactivity refers to an application’s ability to automatically reflect data changes in the user interface.
A non-reactive service returns a one-time value:
// ❌ Non-reactive: if products change, the template does not update
getProducts(): Product[] {
return this.products;
}
Reactivity with RxJS (Subject / BehaviorSubject)
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { Product } from './product.model';
@Injectable({ providedIn: 'root' })
export class ProductsService {
constructor(private httpClient: HttpClient) {}
// Returns an Observable: the component can subscribe and react to changes
getProducts(): Observable<Product[]> {
return this.httpClient.get<Product[]>('/api/products');
}
}
HTTP service pattern flow:
HttpClient.get('/api/products')
│
▼
Observable<Product[]>
│
▼ (subscribe)
CatalogComponent.products
│
▼
Template (*ngFor)
Reactivity with Angular Signals
Since Angular 16, Signals offer a simpler alternative to RxJS for internal service reactivity.
import { Injectable, computed, signal } from '@angular/core';
import { Product } from '@shared/product.model';
@Injectable({ providedIn: 'root' })
export class CartService {
// Writable signal (private)
private cartItems = signal<Product[]>([]);
// Read-only signal exposed to the outside
get cart() {
return this.cartItems.asReadonly();
}
add(product: Product) {
this.cartItems.update((oldCart) => [...oldCart, product]);
}
remove(product: Product) {
this.cartItems.update((oldCart) => oldCart.filter(p => p !== product));
}
// Computed signal: automatically recalculated when cartItems changes
get cartTotal() {
return computed(() => this.cartItems().reduce((prev, next) => {
let discount = next.discount && next.discount > 0 ? 1 - next.discount : 1;
return prev + next.price * discount;
}, 0));
}
}
Key differences: Signal vs Observable
| Characteristic | Signal | Observable (RxJS) |
|---|---|---|
| Syntax | signal(value), .set(), .update() | Subject, .next(), .subscribe() |
| Reading the value | mySignal() (function call) | .subscribe(val => ...) |
| Computed | computed(() => ...) | pipe(map(...)) |
| Ideal scenario | Synchronous local state | Asynchronous streams, HTTP |
| Change detection | Granular and efficient | Zone.js or async pipe |
Service Encapsulation
Problem: exposing mutable properties directly gives too much power to consumers.
// ❌ cart is public and mutable
cart: Product[] = [];
Solution: read-only signal via asReadonly()
// ✅ Proper encapsulation
private cartItems = signal<Product[]>([]);
get cart() {
return this.cartItems.asReadonly(); // No .set() or .update() available
}
HTTP Calls from a Service
Angular proxy configuration (development) in angular.json:
{
"serve": {
"options": {
"proxyConfig": "proxy.conf.json"
}
}
}
proxy.conf.json:
{
"/api": {
"target": "http://localhost:3000",
"secure": false
}
}
Service with HttpClient:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Product } from './product.model';
@Injectable({ providedIn: 'root' })
export class ProductsService {
constructor(private httpClient: HttpClient) {}
getProducts(): Observable<Product[]> {
return this.httpClient.get<Product[]>('/api/products');
}
}
Registration in an NgModule:
// app.module.ts
import { HttpClientModule } from '@angular/common/http';
@NgModule({
imports: [..., HttpClientModule],
})
export class AppModule {}
Standalone application (Angular 15+):
// app.config.ts
import { provideHttpClient } from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(),
]
};
The AsyncPipe
The async pipe allows subscribing directly to an Observable in the template, without manually managing the subscription.
// catalog.component.ts
@Component({ ... })
export class CatalogComponent {
// Observable directly exposed to the template
products: Observable<Product[]> = this.productsService.getProducts();
constructor(private productsService: ProductsService) {}
}
<!-- catalog.component.html -->
<!-- async pipe handles subscribe/unsubscribe automatically -->
<div *ngFor="let product of products | async">
{{ product.name }}
</div>
Advantages of the async pipe:
- No memory leaks (automatic unsubscribe on component destruction)
- Less code in the component
- Compatible with
OnPushchange detection
4. Understanding Angular Dependency Injection
What is DI?
Definition: Dependency Injection is a design pattern that prescribes passing to a class the dependencies it needs, rather than letting it create them itself.
Without DI (manual instantiation):
class ProductComponent {
private repo: ProductRepository;
constructor() {
// The component creates its own dependency
this.repo = new ProductRepository(new DbConnectionService());
}
}
With DI (dependency injection):
class ProductComponent {
// Angular injects the dependency from the outside
constructor(private repo: ProductRepository) {}
}
Benefits of DI
| Benefit | Description |
|---|---|
| Testability | Mocks can be injected in tests without modifying production code |
| Flexibility | One implementation can be substituted for another |
| Modularity | Classes are decoupled from each other |
| Scalability | Easy to add new dependencies |
| Reusability | Services can be shared across modules |
| Reduced complexity | Creation logic is centralized |
Testability example:
// In production
const repo = new ProductRepository(new RealDbConnection());
// In tests
const repo = new ProductRepository(new MockDbConnection());
DI in Angular
Angular implements DI via a system of injectors (IoC containers). The four conceptual steps:
- Mark the class as injectable:
@Injectable() - Register a provider:
providers: [...]orprovidedIn - Declare the dependency: constructor parameter or
inject() - Resolution by the injector: Angular provides the instance
In practice, steps 2–4 are often automatic thanks to providedIn: 'root'.
// Step 1: Mark as injectable
@Injectable({ providedIn: 'root' }) // Step 2: Also defines the provider
export class ProductsService {
getProducts(): Observable<Product[]> { ... }
}
// Step 3: Declare the dependency (Angular resolves step 4 automatically)
@Component({ ... })
export class CatalogComponent {
constructor(private productsService: ProductsService) {} // Injection!
}
5. Angular Service Providers
What is a Provider?
A provider is a set of instructions that tells Angular how to create a service instance. It associates a token (identifier) with a creation recipe.
// Declarative provider in an NgModule
providers: [
{
provide: CartService, // Token = the class itself
useClass: CartService, // Recipe = instantiate this class
}
]
// Equivalent shorthand form
providers: [CartService]
When you write @Injectable({ providedIn: 'root' }), Angular automatically generates this provider in the background.
How Providers Affect Instantiation
Fundamental rule: an injector creates one instance per token (singleton within that injector). Two providers with different tokens = two distinct instances.
// Module A: provider with CartService token
providers: [CartService]
// → Instance A in Module A's injector
// Module B: provider with a custom token
providers: [{ provide: CART_SERVICE_TOKEN, useClass: CartService }]
// → Instance B in Module B's injector (different token = new instance)
useClass, useFactory, useValue
useClass: provides an instance of a class.
providers: [
{ provide: CartService, useClass: CartService }
// Or: use a different class
{ provide: CartService, useClass: MockCartService } // Useful in tests
]
useFactory: uses a factory function to create the instance. Useful when creation requires business logic.
providers: [
{
provide: CartService,
useFactory: () => {
// Custom logic before instantiating the service
const options = { persistenceType: 'local', persistenceKey: 'shopping-cart' };
return new CartService(options);
}
}
]
useValue: provides a direct value (object, primitive, function). Often used with InjectionToken.
export const CART_OPTIONS_TOKEN = new InjectionToken<CartOptions>('CART_OPTIONS');
providers: [
{
provide: CART_OPTIONS_TOKEN,
useValue: { persistenceType: 'local', persistenceKey: 'shopping-cart' }
}
]
InjectionToken and Interfaces
TypeScript interfaces are erased at compilation time (they don’t exist in JavaScript). They therefore cannot be used directly as DI tokens. The solution is InjectionToken.
// shared/products-service.interface.ts
import { InjectionToken } from '@angular/core';
import { Observable } from 'rxjs';
import { Product } from './product.model';
export interface IProductsService {
getProducts(): Observable<Product[]>;
}
// Token usable at runtime
export const IProductsServiceToken = new InjectionToken<IProductsService>('IProductsService');
// squad.module.ts: use the token to provide a different implementation
providers: [
{
provide: IProductsServiceToken,
useClass: EngineersService // Implements IProductsService
}
]
// squad-catalog.component.ts: injection via the token
constructor(
@Inject(IProductsServiceToken) private productsService: IProductsService
) {}
Configurable Service with InjectionToken
Complete example of a configurable CartService with InjectionToken:
// cart.service.ts
import { Inject, Injectable, InjectionToken, computed, signal } from '@angular/core';
import { Product } from '@shared/product.model';
export type CartOptions = {
persistenceType: string; // 'local' | 'none' | 'remote'
persistenceKey: string;
};
export const CART_OPTIONS_TOKEN = new InjectionToken<CartOptions>('CART_OPTIONS');
@Injectable({ providedIn: 'root' })
export class CartService {
private cartItems = signal<Product[]>([]);
constructor(@Inject(CART_OPTIONS_TOKEN) private cartOptions: CartOptions) {
// Hydration from localStorage if configured
if (this.cartOptions?.persistenceType === 'local') {
const cartString = localStorage.getItem(this.cartOptions.persistenceKey);
const cart: Product[] = cartString ? JSON.parse(cartString) as Product[] : [];
this.cartItems.set(cart);
}
}
get cart() {
return this.cartItems.asReadonly();
}
add(product: Product) {
this.cartItems.update((oldCart) => [...oldCart, product]);
this.storeCart();
}
remove(product: Product) {
this.cartItems.update((oldCart) => oldCart.filter(p => p !== product));
this.storeCart();
}
private storeCart() {
if (this.cartOptions?.persistenceType === 'local') {
localStorage.setItem(this.cartOptions.persistenceKey, JSON.stringify(this.cartItems()));
}
}
get cartTotal() {
return computed(() => this.cartItems().reduce((prev, next) => {
let discount = next.discount && next.discount > 0 ? 1 - next.discount : 1;
return prev + next.price * discount;
}, 0));
}
}
In app.module.ts or app.config.ts:
// app.module.ts
@NgModule({
providers: [
{
provide: CART_OPTIONS_TOKEN,
useValue: { persistenceType: 'local', persistenceKey: 'shopping-cart' }
}
]
})
export class AppModule {}
// app.config.ts (standalone)
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(),
{
provide: CART_OPTIONS_TOKEN,
useValue: { persistenceType: 'none', persistenceKey: 'shopping-cart' }
}
]
};
The deps Property
deps allows declaring additional dependencies for a useFactory provider.
providers: [
{
provide: CART_OPTIONS_TOKEN,
useValue: { persistenceType: 'local', persistenceKey: 'shopping-cart' }
},
{
provide: CartService,
useFactory: (cartOptions: CartOptions) => new CartService(cartOptions),
deps: [CART_OPTIONS_TOKEN] // Angular resolves this dependency and passes it to the factory
}
]
The multi Property
multi: true allows having multiple providers for the same token, which will be returned as an array.
providers: [
{ provide: CART_OPTIONS_TOKEN, useValue: { persistenceType: 'local', persistenceKey: 'cart' }, multi: true },
{ provide: CART_OPTIONS_TOKEN, useValue: { persistenceType: 'none', persistenceKey: 'cart' }, multi: true },
]
// Injection will return an array: CartOptions[]
Common use case: NG_VALUE_ACCESSOR for ControlValueAccessor in Angular Forms.
6. Angular Service Injectors
What is an Injector?
An injector is an instance of Angular’s Injector class. It:
- Receives a list of providers (instructions)
- Resolves requested dependencies
- Caches created instances (singleton within that injector)
- Delegates to parent injectors if the dependency is not found locally
The Injector Hierarchy
Angular has multiple injectors organized in a tree:
Environment Injectors (application level)
| Injector | Created by | Usage |
|---|---|---|
| NullInjector | Bootstrap | Throws an error if no dependency found (@Optional() returns null) |
| PlatformInjector | platformBrowserDynamic() | Services shared across multiple Angular apps on the same page |
| RootInjector | Bootstrap | Global services (providedIn: 'root') |
| Module/Route Injector | Module or lazy-loaded route | Services specific to a module or route |
Element Injectors (component level)
Each component can have its own injector via its providers property.
Resolution Algorithm
Component requests a dependency
│
▼
Element Injector of the component
│ (if not found)
▼
Element Injector of the parent component
│ (if not found, travels up the hierarchy...)
▼
Root Injector (Environment)
│ (if not found)
▼
Platform Injector
│ (if not found)
▼
NullInjector → Error: "No provider found for X"
Configuring Injectors
providedIn: 'root' — global singleton in the root injector:
@Injectable({ providedIn: 'root' })
export class ProductsService {}
providedIn: 'platform' — shared across multiple Angular apps (rare):
@Injectable({ providedIn: 'platform' })
export class SharedPlatformService {}
Module NgModule — scope limited to the module (and its children):
@NgModule({
providers: [SomeModuleService]
})
export class FeatureModule {}
Lazy-loaded route — new injector created for this route scope:
// app-routing.module.ts
{
path: 'squad',
loadChildren: () => import('./squad/squad.module').then(m => m.SquadModule),
}
// squad.module.ts
@NgModule({
providers: [
CartService, // Isolated new instance for the squad module
{ provide: CART_OPTIONS_TOKEN, useValue: { persistenceType: 'none', persistenceKey: 'squad' } }
]
})
export class SquadModule {}
Standalone routes — providers at the route level:
// app.routes.ts
{
path: 'squad',
providers: [
CartService,
{ provide: CART_OPTIONS_TOKEN, useValue: { persistenceType: 'none', persistenceKey: 'squad' } }
],
loadChildren: () => import('./squad/squad.routes').then(r => r.SQUAD_ROUTES)
}
Component-Level Providers
Providers can be defined directly in a component’s metadata:
@Component({
selector: 'bot-squad-catalog',
providers: [
// This component uses EngineersService, not ProductsService
{
provide: IProductsServiceToken,
useClass: EngineersService
}
]
})
export class SquadCatalogComponent {
constructor(
@Inject(IProductsServiceToken) private productsService: IProductsService
) {}
}
Important: component-level providers create a new instance of the service for each instance of the component (and its direct children).
Resolution Modifiers: @Self, @SkipSelf, @Host, @Optional
These decorators modify the dependency resolution algorithm:
| Decorator | Behavior |
|---|---|
@Self() | The dependency must be provided by the component’s own injector (not from parents). Error if absent. |
@SkipSelf() | Ignores the component’s own injector and starts resolution at the parent |
@Host() | Limits the search up to the component’s host element (view boundary) |
@Optional() | If the dependency is not found, injects null instead of throwing an error |
@Component({
providers: [
{ provide: IProductsServiceToken, useClass: EngineersService }
]
})
export class SquadCatalogComponent {
constructor(
// @Self(): MUST be in THIS component's providers
@Self() @Inject(IProductsServiceToken) private service: IProductsService,
// @Optional(): null if not found, no error
@Optional() private optionalService?: SomeOptionalService
) {}
}
Standalone Applications: Route Injectors and Global Providers
Providers at the route level (equivalent to lazy-loaded module)
// app.routes.ts
export const routes: Routes = [
{
path: 'squad',
providers: [
// Providers specific to this route and its children
{
provide: CART_OPTIONS_TOKEN,
useValue: { persistenceType: 'none', persistenceKey: 'squad' }
},
{
provide: IProductsServiceToken,
useClass: EngineersService
}
],
loadComponent: () => import('./squad/squad-catalog.component')
.then(c => c.SquadCatalogComponent)
}
];
Global providers in app.config.ts
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { CART_OPTIONS_TOKEN } from './catalog/cart.service';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(),
{
provide: CART_OPTIONS_TOKEN,
useValue: { persistenceType: 'none', persistenceKey: 'cart' }
}
]
};
Note: providedIn: 'root' remains the best practice for the majority of services in standalone apps. app.config.ts is used for value providers (config, options) and Angular module providers (provideRouter, provideHttpClient).
7. Architecture Diagrams
Angular Injector Tree
graph TD
NULL["NullInjector\n(Throws error or null if @Optional)"]
PLATFORM["PlatformInjector\n(providedIn: 'platform')"]
ROOT["RootInjector\n(providedIn: 'root')"]
MODULE["Module/Route Injector\n(providers: [] in lazy NgModule / route)"]
COMP["Element Injector\n(providers: [] in @Component)"]
CHILD["Child Element Injector"]
NULL --> PLATFORM --> ROOT --> MODULE --> COMP --> CHILD
style NULL fill:#f66,color:#fff
style ROOT fill:#4a9,color:#fff
style MODULE fill:#59b,color:#fff
style COMP fill:#a74,color:#fff
DI Hierarchy: Resolution Flow
flowchart LR
C[CatalogComponent\nrequests CartService]
EI[Element Injector\nof the component]
RI[Root Injector]
INST[CartService Instance\ncached in Root]
C -->|1. Looks in| EI
EI -->|2. Not found, moves up| RI
RI -->|3. Found! Returns| INST
INST -->|4. Injected into| C
HTTP Service Pattern
sequenceDiagram
participant C as CatalogComponent
participant S as ProductsService
participant H as HttpClient
participant API as API Server
C->>S: getProducts()
S->>H: get('/api/products')
H->>API: GET /api/products
API-->>H: JSON Response
H-->>S: Observable<Product[]>
S-->>C: Observable<Product[]>
C->>C: subscribe() or async pipe
C->>C: Displays products
Multiple Instances with Module Injectors
graph TB
subgraph RootInjector["Root Injector"]
CS1["CartService Instance #1\n(providedIn: 'root')"]
end
subgraph SquadModule["Squad Module Injector"]
CS2["CartService Instance #2\n(providers: [CartService] in SquadModule)"]
end
CatalogComp["CatalogComponent"] -->|"injects"| CS1
CartComp["CartComponent"] -->|"injects"| CS1
SearchComp["SearchComponent"] -->|"injects"| CS1
SquadComp["SquadCatalogComponent"] -->|"injects"| CS2
SquadCart["SquadRosterComponent"] -->|"injects"| CS2
8. Reference Tables
providedIn Scopes
| Value | Scope | Use case |
|---|---|---|
'root' | Entire application | Global services (singleton) — recommended |
'platform' | Platform (multiple apps on the page) | Services shared between micro-frontends |
'any' | Deprecated | |
| Module class | Specific module | Tree-shakable in a specific module |
Provider Types
| Property | Description | Example |
|---|---|---|
useClass | Instantiates a class | { provide: CartService, useClass: CartService } |
useExisting | Alias to an existing token | { provide: Logger, useExisting: ConsoleLogger } |
useFactory | Factory function | { provide: S, useFactory: () => new S() } |
useValue | Direct value | { provide: API_URL, useValue: 'https://...' } |
DI Decorators
| Decorator | Import | Description |
|---|---|---|
@Injectable() | @angular/core | Marks a class as injectable + DI metadata |
@Inject(token) | @angular/core | Specifies a non-class token for injection |
@Self() | @angular/core | Resolution only in the local injector |
@SkipSelf() | @angular/core | Ignores local injector, starts at parent |
@Host() | @angular/core | Limits resolution up to the host element |
@Optional() | @angular/core | Returns null if not found (no error) |
Comparison: Module-based vs Standalone
| Aspect | Module-based | Standalone |
|---|---|---|
| Global provider | AppModule.providers[] | app.config.ts → ApplicationConfig.providers[] |
| Lazy route provider | NgModule.providers[] | Route.providers[] in app.routes.ts |
| Bootstrap | bootstrapModule(AppModule) | bootstrapApplication(AppComponent, appConfig) |
| HTTP | HttpClientModule import | provideHttpClient() |
| Router | RouterModule.forRoot(routes) | provideRouter(routes) |
providedIn: 'root' | ✅ Identical | ✅ Identical |
9. Best Practices and Summary
Key Takeaways
-
providedIn: 'root'is the standard way to create singleton services. Do not declare in a module’sproviders: []unless there is a specific need. -
Single responsibility: one service = one responsibility. Do not mix data retrieval, business logic, and state management in the same service.
-
Encapsulate state with
asReadonly()Signals or Observables. Never directly expose mutable arrays. -
InjectionTokenfor configuration values (options, URLs, constants) that are not classes. -
Multiple instances: use lazy-loaded modules (or routes with
providers) to create isolated service instances in specific areas of the application. -
@Optional()for optional dependencies — prevents crashes if the provider is not configured. -
Interfaces +
InjectionTokenfor abstract dependencies. TypeScript interfaces do not exist at runtime, so they cannot serve as DI tokens. -
asyncpipe in templates rather than.subscribe()in the component — automatic memory management.
Anti-Patterns to Avoid
// ❌ Manual instantiation (bypasses DI)
const service = new ProductsService(new HttpClient(...));
// ❌ Exposing a writable signal publicly
public cartItems = signal<Product[]>([]);
// ❌ State in the component when it should be shared
addToCart(product: Product) {
this.localCart.push(product); // Invisible to other components
}
// ❌ HTTP retrieval logic in the component
ngOnInit() {
fetch('/api/products').then(r => r.json()).then(data => this.products = data);
}
Service Creation Checklist
-
@Injectable({ providedIn: 'root' })or appropriate scope - Explicit typing of public methods
- Internal state exposed via
asReadonly()(Signal) or Observable - Public methods to modify state (
add(),remove(), etc.) -
InjectionTokenif configuration options are needed - Error handling on HTTP calls (
catchError)
Search Terms
angular · services · dependency · injection · frontend · development · service · injectors · providers · injector · level · reactivity · resolution · global · hierarchy · http · injectiontoken · property · provider · route · standalone