Angular 19 🅰️ Services and Basic Dependency Injection
A service is a class that holds shared logic — data access, business rules, state, communication with APIs. Dependency injection (DI) is how Angular gives components and other services access to those services without them creating instances manually. Instead of new UserService() inside every component, you declare a dependency and Angular provides an instance. That inversion — the framework provides, not the class creates — is what makes services testable, replaceable, and singletons by default. This chapter covers what services are, how to create and provide them, how to inject them, and why DI exists at all.
Key point: A service is a class decorated with @Injectable(). A component or service receives a service through its constructor or via inject() — it never instantiates it. Angular’s DI system reads the dependency graph, creates instances as needed, and hands them out. By default, providedIn: 'root' makes a service a singleton across the whole app. The result: shared state, testable code, and no manual wiring.
What a service is
A service is a class that provides functionality to other parts of the app. It’s a plain TypeScript class with @Injectable() — no template, no selector, no lifecycle.
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class UserService {
private users: User[] = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
];
getUsers(): User[] {
return this.users;
}
addUser(user: User): void {
this.users.push(user);
}
}
UserService holds a list of users and methods to read and add. It’s not a component — it has no template. It’s just a class with @Injectable().
What services are for:
- Data access — HTTP calls, local storage, IndexedDB
- Business logic — calculations, validations, transformations
- State — shared app state, caches, session data
- Communication — between unrelated components
- Utilities — logging, notifications, formatting
What services are not:
- Components — services have no template
- Directives — no host element
- Pipes — no transform method
- Everything — services shouldn’t be a dumping ground
Why services exist: Components should focus on presentation. Business logic, data fetching, and shared state don’t belong in a component. Extracting them to services keeps components small, makes the logic testable in isolation, and lets multiple components share the same code.
A service without @Injectable:
export class UserService {
getUsers(): User[] { return []; }
}
This works as a plain class but can’t be injected — Angular needs @Injectable to know it’s a service. Without it, DI fails.
Why “service”: The name is historical — a service is something that provides a service to other code. It’s not a technical term in Angular; it’s a naming convention for “class that isn’t a component, directive, or pipe.” Any class decorated with
@Injectable()is a service.
@Injectable() — the decorator
@Injectable() marks a class as available for DI.
@Injectable()
export class LoggerService {
log(msg: string): void {
console.log(msg);
}
}
The decorator without providedIn:
@Injectable()
export class LoggerService {}
This declares the service but doesn’t say where it’s provided. It must be added to a providers array somewhere — a module, a component, or bootstrapApplication.
With providedIn: 'root':
@Injectable({ providedIn: 'root' })
export class LoggerService {}
The service provides itself at the root injector. No providers array needed.
providedIn options:
| Value | Scope |
|---|---|
'root' | App-wide singleton (most common) |
'platform' | Across multiple apps on the page |
'any' | New instance per module that injects it |
SomeModule | Scoped to a specific module |
| (omitted) | Must be provided explicitly |
Why providedIn: 'root' is the default choice:
- Singleton — one instance shared across the app
- Tree-shakable — if nothing injects it, it’s dropped from the bundle
- Simple — no providers array needed
- Lazy-safe — still a singleton across lazy-loaded modules
What @Injectable() does: It tells Angular the class can have dependencies injected and can be injected. Even a service with no dependencies needs it if it’s going to be injected.
Why
providedIn: 'root'is idiomatic: It’s the modern way to register a service. One line, tree-shakable, app-wide singleton. Older Angular required every service to be added to a module’sprovidersarray.providedIn: 'root'made that unnecessary. New services should almost always use it.
Creating a service
The CLI generates a service with the correct structure.
ng generate service user
# or
ng g s user
This creates:
src/app/user.service.ts
src/app/user.service.spec.ts
Generated file:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class UserService {
constructor() { }
}
The providedIn: 'root' is included by default — the modern pattern.
Flags:
| Flag | Effect |
|---|---|
--skip-tests | No spec file |
--flat | No folder |
--providedIn=root | Explicit provider (default) |
--providedIn=platform | Platform scope |
--providedIn=any | Per-module instances |
Adding methods:
@Injectable({ providedIn: 'root' })
export class UserService {
private users: User[] = [];
getUsers(): User[] {
return this.users;
}
getUser(id: number): User | undefined {
return this.users.find(u => u.id === id);
}
addUser(user: User): void {
this.users.push(user);
}
removeUser(id: number): void {
this.users = this.users.filter(u => u.id !== id);
}
}
The service holds state and exposes methods. Components call those methods.
Naming convention: XxxService — UserService, AuthService, LoggerService. The suffix makes services easy to distinguish from components.
Why the CLI: It generates the correct structure with providedIn: 'root' and creates the spec file. Hand-writing services is fine, but the CLI ensures consistency.
Why
Servicesuffix: It’s the Angular convention. Components end inComponent, directives inDirective, pipes inPipe, and services inService. The naming makes it obvious at a glance what a file contains. Consistency across a codebase is worth the small overhead.
Injecting a service
A component receives a service through constructor injection or inject().
Constructor injection:
import { Component } from '@angular/core';
import { UserService } from './user.service';
@Component({
selector: 'app-user-list',
standalone: true,
template: `
@for (user of users; track user.id) {
<p>{{ user.name }}</p>
}
`
})
export class UserListComponent {
users: User[];
constructor(private userService: UserService) {
this.users = userService.getUsers();
}
}
private userService: UserService in the constructor declares the dependency. Angular provides the instance when the component is created.
The inject() function — the modern alternative:
import { Component, inject } from '@angular/core';
import { UserService } from './user.service';
@Component({
selector: 'app-user-list',
standalone: true,
template: `...`
})
export class UserListComponent {
private userService = inject(UserService);
users = this.userService.getUsers();
}
inject(UserService) returns the instance. No constructor needed.
Comparing the two:
| Aspect | Constructor | inject() |
|---|---|---|
| Where | Constructor parameter | Field initializer or constructor body |
| Readability | Clear signature | Less boilerplate |
| Works in | Constructor | Injection context |
| Recommended | Legacy | Modern |
| With signals | Awkward | Natural |
inject() must be called in an injection context:
- The constructor
- A field initializer
- A factory function
- Not in a method or after
await
// ✅ Field initializer
private userService = inject(UserService);
// ✅ Constructor
constructor() {
const userService = inject(UserService);
}
// ❌ In a method
ngOnInit(): void {
const userService = inject(UserService); // ❌ not allowed
}
For ngOnInit, store the service as a field first.
Why inject() is preferred: It’s shorter, works better with signals, and can be used in more contexts (factory functions, other services). Constructor injection still works — it’s not deprecated — but new code should use inject().
Why two ways: Constructor injection was the only option until Angular 14.
inject()was added to support standalone components and signal-based patterns where a constructor wasn’t always convenient. Both work;inject()is where the framework is heading.
Service scopes
Where a service is provided determines its scope — how many instances exist and where they’re shared.
providedIn: 'root' — the singleton:
@Injectable({ providedIn: 'root' })
export class UserService {}
One instance for the entire app. Every component that injects UserService gets the same one.
Component-level providers:
@Component({
selector: 'app-user-list',
standalone: true,
providers: [UserService],
template: `...`
})
export class UserListComponent {
private userService = inject(UserService);
}
The service is provided at the component level. Each instance of the component gets its own UserService. Other components get a different instance (or none).
Module-level providers (legacy):
@NgModule({
providers: [UserService]
})
export class UserModule {}
The service is scoped to the module. Components in the module share one instance.
Scope comparison:
| Provider | Instances |
|---|---|
providedIn: 'root' | One per app |
Module providers | One per module injector |
Component providers | One per component instance |
| Lazy-loaded module | One per lazy load |
Why scope matters: Shared state needs a single instance. Component-specific state needs a per-component instance. The scope determines which.
Common patterns:
- Singleton services (
providedIn: 'root') — auth, config, global state - Feature-scoped services (module providers) — feature state
- Component-scoped services (component providers) — per-instance state
Example of a component-scoped service:
@Injectable()
export class FormStateService {
private dirty = false;
markDirty(): void { this.dirty = true; }
isDirty(): boolean { return this.dirty; }
}
@Component({
selector: 'app-form',
providers: [FormStateService],
template: `...`
})
export class FormComponent {
state = inject(FormStateService);
}
Each FormComponent instance gets its own FormStateService. State doesn’t leak between forms.
Why scope affects lazy loading: A service with providedIn: 'root' is a singleton even across lazy-loaded modules. A service provided in a lazy module gets a new instance when that module is loaded.
Why most services are root-scoped: Shared services — API clients, auth, logging — need one instance everywhere.
providedIn: 'root'gives that. Component-scoped services are for state that belongs to a single component instance. Module-scoped is legacy; new code uses the other two.
Why DI matters
Dependency injection exists to solve a specific problem: how do you give a class what it needs without it creating those things itself?
Without DI:
export class UserListComponent {
private userService = new UserService();
// tightly coupled to a specific implementation
}
With DI:
export class UserListComponent {
constructor(private userService: UserService) {}
// the dependency is provided, not created
}
The benefits:
- Testability — pass a mock
UserServicein tests - Flexibility — swap implementations without changing the consumer
- Singletons by default — one instance shared, no manual management
- Lifecycle management — Angular creates and destroys services with the app
- Decoupling — the consumer depends on an abstraction, not a concrete construction
Testability example:
// Production
const service = new UserService(http);
// Test
const service = new MockUserService();
The component doesn’t care which — it just uses what it’s given.
Swapping implementations:
@Injectable()
export class Logger {
log(msg: string): void { console.log(msg); }
}
@Injectable()
export class RemoteLogger extends Logger {
override log(msg: string): void {
fetch('/api/logs', { method: 'POST', body: msg });
}
}
// Provide RemoteLogger instead of Logger
providers: [{ provide: Logger, useClass: RemoteLogger }]
The consumer injects Logger and gets RemoteLogger. No code changes in the consumer.
Why this matters for large apps: Every service is a seam for testing and replacement. Authentication can be mocked in tests. Logging can be swapped for production. Configuration can differ per environment. All without touching the code that uses the service.
Why DI is a core Angular concept: It’s the mechanism that ties everything together. Components need services, services need other services, and DI wires the graph. Without it, every class would manage its own dependencies, and the app would be untestable and brittle. DI inverts control — the framework decides what to create and when.
Providing a service
There are several ways to provide a service. Each has a different scope.
Self-provided with providedIn:
@Injectable({ providedIn: 'root' })
export class UserService {}
The service provides itself.
In a component:
@Component({
providers: [UserService]
})
export class UserListComponent {}
Scoped to the component.
In bootstrapApplication:
bootstrapApplication(AppComponent, {
providers: [UserService]
});
Same as root-scoped.
Using a provider object:
providers: [
{ provide: Logger, useClass: ConsoleLogger }
]
Maps a token to a class.
Provider object forms:
| Form | Meaning |
|---|---|
useClass | Instantiate a class |
useValue | Use a fixed value |
useFactory | Call a function |
useExisting | Alias another token |
useClass:
providers: [
{ provide: Logger, useClass: RemoteLogger }
]
When Logger is injected, Angular creates RemoteLogger.
useValue:
providers: [
{ provide: API_URL, useValue: 'https://api.example.com' }
]
The token resolves to the given value.
useFactory:
providers: [
{
provide: Config,
useFactory: () => new Config(process.env.NODE_ENV)
}
]
The factory function produces the value.
useExisting:
providers: [
{ provide: Logger, useClass: ConsoleLogger },
{ provide: LoggingService, useExisting: Logger }
]
Both tokens resolve to the same instance.
InjectionToken for non-class tokens:
import { InjectionToken } from '@angular/core';
export const API_URL = new InjectionToken<string>('API_URL');
providers: [
{ provide: API_URL, useValue: 'https://api.example.com' }
]
// Inject
private apiUrl = inject(API_URL);
InjectionToken is for tokens that aren’t classes — strings, objects, configuration.
Why multiple forms: Different needs. useClass for implementations, useValue for config, useFactory for computed values, useExisting for aliases. Together they let DI satisfy almost any dependency.
Why
providedIn: 'root'is enough most of the time: It covers the common case — a singleton service with no custom configuration. Provider objects are for when you need to swap implementations, provide configuration, or scope to a component. Most services useprovidedIn: 'root'; the rest use provider objects.
A full example
A small app with services for data, logging, and configuration.
// ============================================
// TYPES
// ============================================
export interface User {
id: number;
name: string;
email: string;
}
// ============================================
// CONFIGURATION TOKEN
// ============================================
import { InjectionToken } from '@angular/core';
export interface AppConfig {
apiUrl: string;
logLevel: 'debug' | 'info' | 'error';
}
export const APP_CONFIG = new InjectionToken<AppConfig>('APP_CONFIG');
// ============================================
// LOGGER SERVICE
// ============================================
import { Injectable, inject } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class LoggerService {
private config = inject(APP_CONFIG);
debug(msg: string): void {
if (this.config.logLevel === 'debug') console.debug(msg);
}
info(msg: string): void {
console.info(msg);
}
error(msg: string): void {
console.error(msg);
}
}
// ============================================
// USER SERVICE
// ============================================
@Injectable({ providedIn: 'root' })
export class UserService {
private logger = inject(LoggerService);
private config = inject(APP_CONFIG);
private users: User[] = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' }
];
getUsers(): User[] {
this.logger.debug('Getting users');
return this.users;
}
getUser(id: number): User | undefined {
this.logger.debug(`Getting user ${id}`);
return this.users.find(u => u.id === id);
}
async addUser(user: Omit<User, 'id'>): Promise<User> {
const newUser: User = { ...user, id: this.users.length + 1 };
this.users.push(newUser);
this.logger.info(`Added user: ${newUser.name}`);
return newUser;
}
}
// ============================================
// COMPONENT — INJECTS SERVICES
// ============================================
import { Component, inject, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-user-list',
standalone: true,
imports: [CommonModule],
template: `
<h2>Users</h2>
<ul>
@for (user of users(); track user.id) {
<li>{{ user.name }} — {{ user.email }}</li>
}
</ul>
`
})
export class UserListComponent {
private userService = inject(UserService);
users = signal<User[]>(this.userService.getUsers());
}
// ============================================
// BOOTSTRAP
// ============================================
import { bootstrapApplication } from '@angular/platform-browser';
bootstrapApplication(AppComponent, {
providers: [
{ provide: APP_CONFIG, useValue: { apiUrl: '/api', logLevel: 'debug' } }
]
});
What this shows:
APP_CONFIG— anInjectionTokenwith a value provided at bootstrapLoggerService— a service withprovidedIn: 'root', injecting the config tokenUserService— a service injecting both the logger and configUserListComponent— injectingUserServiceviainject()
Every dependency is provided by the framework. No new anywhere. Swapping the logger or config affects all services without changing their code.
Why this shape: It’s a realistic service graph. Config at the root, logger in the middle, data service at the top, component consuming.
inject()reads naturally, the token provides configuration, and the services share state through the singleton scope.
Complete Example Session
# ============================================
# PART 1: GENERATE A SERVICE
# ============================================
ng generate service user
# [ CREATE src/app/user.service.ts ]
# [ CREATE src/app/user.service.spec.ts ]
cat src/app/user.service.ts
# [ import { Injectable } from '@angular/core'; ]
# [ ]
# [ @Injectable({ providedIn: 'root' }) ]
# [ export class UserService { ]
# [ constructor() { } ]
# [ } ]
# ============================================
# PART 2: WRITE THE SERVICE
# ============================================
cat > src/app/user.service.ts << 'EOF'
import { Injectable } from '@angular/core';
export interface User {
id: number;
name: string;
}
@Injectable({ providedIn: 'root' })
export class UserService {
private users: User[] = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
];
getUsers(): User[] {
return this.users;
}
addUser(user: User): void {
this.users.push(user);
}
}
EOF
# ============================================
# PART 3: INJECT VIA CONSTRUCTOR
# ============================================
cat > src/app/user-list.component.ts << 'EOF'
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { UserService } from './user.service';
@Component({
selector: 'app-user-list',
standalone: true,
imports: [CommonModule],
template: `
<ul>
@for (user of users; track user.id) {
<li>{{ user.name }}</li>
}
</ul>
`
})
export class UserListComponent {
users;
constructor(private userService: UserService) {
this.users = userService.getUsers();
}
}
EOF
npx tsc --noEmit src/app/user-list.component.ts
# (no errors)
# ============================================
# PART 4: INJECT VIA inject()
# ============================================
cat > src/app/user-list.component.ts << 'EOF'
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { UserService } from './user.service';
@Component({
selector: 'app-user-list',
standalone: true,
imports: [CommonModule],
template: `
<ul>
@for (user of users; track user.id) {
<li>{{ user.name }}</li>
}
</ul>
`
})
export class UserListComponent {
private userService = inject(UserService);
users = this.userService.getUsers();
}
EOF
npx tsc --noEmit src/app/user-list.component.ts
# (no errors)
# ============================================
# PART 5: INJECTION TOKEN
# ============================================
cat > src/app/config.ts << 'EOF'
import { InjectionToken } from '@angular/core';
export interface AppConfig {
apiUrl: string;
}
export const APP_CONFIG = new InjectionToken<AppConfig>('APP_CONFIG');
EOF
cat > src/app/logger.service.ts << 'EOF'
import { Injectable, inject } from '@angular/core';
import { APP_CONFIG } from './config';
@Injectable({ providedIn: 'root' })
export class LoggerService {
private config = inject(APP_CONFIG);
log(msg: string): void {
console.log(`[${this.config.apiUrl}] ${msg}`);
}
}
EOF
# ============================================
# PART 6: PROVIDE AT BOOTSTRAP
# ============================================
cat > src/main.ts << 'EOF'
import { bootstrapApplication } from '@angular/platform-browser';
import { APP_CONFIG } from './app/config';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, {
providers: [
{ provide: APP_CONFIG, useValue: { apiUrl: '/api' } }
]
});
EOF
npx tsc --noEmit src/main.ts
# (no errors)
# ============================================
# PART 7: TEST WITH MOCK
# ============================================
cat > src/app/user.service.spec.ts << 'EOF'
import { TestBed } from '@angular/core/testing';
import { UserService } from './user.service';
describe('UserService', () => {
let service: UserService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(UserService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should return users', () => {
expect(service.getUsers().length).toBe(2);
});
});
EOF
echo "Run tests: ng test"
Quick Reference
@Injectable Options
| Option | Scope |
|---|---|
providedIn: 'root' | App-wide singleton |
providedIn: 'platform' | Across apps |
providedIn: 'any' | Per module |
providedIn: SomeModule | Module-scoped |
| (omitted) | Must be in providers |
Injection Methods
| Method | Syntax |
|---|---|
| Constructor | constructor(private s: Service) {} |
inject() | private s = inject(Service) |
| Field initializer | private s = inject(Service) |
| Bootstrap | providers: [Service] |
Provider Object Forms
| Form | Purpose |
|---|---|
useClass | Instantiate a class |
useValue | Provide a fixed value |
useFactory | Compute via function |
useExisting | Alias another token |
CLI Commands
| Command | Creates |
|---|---|
ng g s NAME | Service + spec |
ng g s NAME --skip-tests | Service only |
ng g s NAME --flat | No folder |
ng g s NAME --providedIn=any | Per-module scope |
Service Scopes
| Scope | Instance count |
|---|---|
| Root | 1 per app |
| Platform | 1 per browser page |
| Module | 1 per module injector |
| Component | 1 per component instance |
| Lazy-loaded module | 1 per load |
Where to Provide
| Location | Scope |
|---|---|
@Injectable({ providedIn: 'root' }) | App-wide |
@Component({ providers: [] }) | Component instance |
@NgModule({ providers: [] }) | Module |
bootstrapApplication providers | Root |
useFactory in a route | Route-scoped |
Injection Context
| Location | inject() works |
|---|---|
| Constructor | ✅ |
| Field initializer | ✅ |
| Factory function | ✅ |
ngOnInit | ❌ |
| Method | ❌ |
After await | ❌ |
Token Types
| Token | For |
|---|---|
| Class | Typical services |
InjectionToken | Non-class values |
| String | Not recommended |
| Symbol | Rare |
Common Patterns
| Pattern | Code |
|---|---|
| Singleton | providedIn: 'root' |
| Component-scoped | providers: [Service] in @Component |
| Config token | new InjectionToken<T>('X') |
| Interface mapping | { provide: I, useClass: C } |
| Alias | { provide: A, useExisting: B } |
| Test mock | { provide: Service, useValue: mock } |
Bootstrap Example
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes),
provideHttpClient(),
{ provide: APP_CONFIG, useValue: { apiUrl: '/api' } }
]
});
Testing
| Step | Code |
|---|---|
| Configure | TestBed.configureTestingModule({ providers: [...] }) |
| Inject | TestBed.inject(Service) |
| Mock | { provide: Service, useValue: mock } |
Common Errors
| Error | Cause | Fix |
|---|---|---|
No provider for X | Service not registered | Add providedIn or providers |
inject() outside context | Called in method | Move to field |
NullInjectorError | Missing provider | Provide at root or component |
| Circular dependency | A needs B needs A | Extract or use forwardRef |
Scoping Guide
| Need | Scope |
|---|---|
| Auth, config, logging | Root |
| Feature state | Module or root |
| Per-form state | Component |
| Cache per component | Component |
| Route-scoped data | Route providers |
Best Practices
✅ Do This:
// Use providedIn: 'root' for shared services
@Injectable({ providedIn: 'root' })
export class UserService {} // ✅
// Use inject() for modern code
private userService = inject(UserService); // ✅
// Name services with the Service suffix
export class LoggerService {} // ✅
// Use InjectionToken for config
export const API_URL = new InjectionToken<string>('API_URL');// ✅
// Provide component-scoped services when state is per-component
@Component({ providers: [FormStateService] }) // ✅
// Use provider objects to swap implementations
{ provide: Logger, useClass: RemoteLogger } // ✅
// Test with mock providers
{ provide: UserService, useValue: mockService } // ✅
// Inject in field initializers
private config = inject(APP_CONFIG); // ✅
// Use signals with inject
users = signal(inject(UserService).getUsers()); // ✅
❌ Don’t Do This:
// Don't instantiate services manually
private userService = new UserService(); // ❌ not testable // ❌
// Don't forget @Injectable
export class UserService {} // ⚠️ can't be injected // ⚠️
// Don't call inject() outside an injection context
ngOnInit(): void {
const s = inject(UserService); // ❌ // ❌
}
// Don't inject in methods
someMethod(): void {
const s = inject(UserService); // ❌ // ❌
}
// Don't provide the same service at multiple levels unnecessarily
// ⚠️ can lead to duplicate state // ⚠️
// Don't use strings as tokens
{ provide: 'apiUrl', useValue: '...' } // ⚠️ use InjectionToken // ⚠️
// Don't put all logic in services
// Components can have presentation logic // ⚠️
// Don't inject services into unrelated helpers
// Pure functions are better for pure logic // ⚠️
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
Missing @Injectable | Not injectable | Add decorator |
Missing providedIn | No provider | Add or register |
inject() in method | Outside context | Move to field |
| Multiple instances | Wrong scope | Use providedIn: 'root' |
| Circular dependency | A ↔ B | Extract shared logic |
| String tokens | No type safety | InjectionToken |
Manual new | Untestable | Inject instead |
Forgetting providers | No provider error | Add to array |
| Over-scoping | Duplicate state | Use root for shared |
| Under-scoping | Lost state | Component for per-instance |
Real-World Examples
1. Basic service
@Injectable({ providedIn: 'root' })
export class LoggerService {}
2. Service with state
@Injectable({ providedIn: 'root' })
export class CartService {
private items: Item[] = [];
add(item: Item): void { this.items.push(item); }
all(): Item[] { return this.items; }
}
3. HTTP service
@Injectable({ providedIn: 'root' })
export class ApiService {
private http = inject(HttpClient);
get<T>(url: string): Observable<T> {
return this.http.get<T>(url);
}
}
4. Inject via constructor
constructor(private userService: UserService) {}
5. Inject via inject()
private userService = inject(UserService);
6. Injection token
const API_URL = new InjectionToken<string>('API_URL');
7. Provide a value
{ provide: API_URL, useValue: '/api' }
8. Provide a class
{ provide: Logger, useClass: ConsoleLogger }
9. Provide a factory
{
provide: CONFIG,
useFactory: () => ({ env: 'dev' })
}
10. Alias a provider
{ provide: LoggingService, useExisting: Logger }
11. Component-scoped service
@Component({ providers: [FormStateService] })
12. Root-scoped service
@Injectable({ providedIn: 'root' })
13. Service injecting service
@Injectable({ providedIn: 'root' })
export class UserService {
private logger = inject(LoggerService);
}
14. Bootstrap providers
bootstrapApplication(App, {
providers: [{ provide: APP_CONFIG, useValue: {} }]
});
15. Testing a service
TestBed.configureTestingModule({});
const service = TestBed.inject(UserService);
16. Mocking in tests
{ provide: UserService, useValue: { getUsers: () => [] } }
17. Signal state service
@Injectable({ providedIn: 'root' })
export class StateService {
count = signal(0);
increment() { this.count.update(c => c + 1); }
}
18. Lazy-loaded service
@Injectable()
export class FeatureService {}
// Provided in a lazy module — new instance per load
19. Optional injection
private logger = inject(LoggerService, { optional: true });
20. Default value
private config = inject(APP_CONFIG, { optional: true }) ?? defaultConfig;
Visual: Service Injection Flow
┌──────────────────────────────────────────────┐
│ Service │
│ │
│ @Injectable({ providedIn: 'root' }) │
│ export class UserService { } │
│ │
└──────────────────────────────────────────────┘
│
│ registered with DI
▼
┌──────────────────────────────────────────────┐
│ Angular Injector │
│ │
│ Root injector holds the singleton │
│ │
└──────────────────────────────────────────────┘
│
│ provided to
▼
┌──────────────────────────────────────────────┐
│ Component │
│ │
│ private userService = inject(UserService); │
│ │
│ → same instance across the app │
│ │
└──────────────────────────────────────────────┘
Visual: Two Injection Styles
┌──────────────────────────────────────────────┐
│ Constructor injection │
│ │
│ export class C { │
│ constructor(private s: Service) {} │
│ } │
│ │
│ ─ In constructor only │
│ ─ Clear signature │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ inject() function │
│ │
│ export class C { │
│ private s = inject(Service); │
│ } │
│ │
│ ─ Field initializer │
│ ─ Less boilerplate │
│ ─ Modern default │
│ │
└──────────────────────────────────────────────┘
Visual: Scopes
┌──────────────────────────────────────────────┐
│ Root (providedIn: 'root') │
│ │
│ ┌────────────────────────────────────────┐ │
│ │ UserService (single instance) │ │
│ └────────────────────────────────────────┘ │
│ ▲ ▲ ▲ │
│ │ │ │ │
│ ┌────┴───┐ ┌────┴───┐ ┌────┴───┐ │
│ │ Comp A │ │ Comp B │ │ Comp C │ │
│ └────────┘ └────────┘ └────────┘ │
│ │
│ All share the same instance │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Component providers │
│ │
│ ┌─────────────────────────┐ │
│ │ Comp A │ │
│ │ providers: [S] │ │
│ │ ┌──────────────────┐ │ │
│ │ │ S (instance 1) │ │ │
│ │ └──────────────────┘ │ │
│ └─────────────────────────┘ │
│ │
│ ┌─────────────────────────┐ │
│ │ Comp A (another) │ │
│ │ providers: [S] │ │
│ │ ┌──────────────────┐ │ │
│ │ │ S (instance 2) │ │ │
│ │ └──────────────────┘ │ │
│ └─────────────────────────┘ │
│ │
│ Each instance gets its own │
│ │
└──────────────────────────────────────────────┘
Visual: Provider Object Forms
┌──────────────────────────────────────────────┐
│ useClass │
│ │
│ { provide: Logger, useClass: RemoteLogger } │
│ │
│ Logger → new RemoteLogger() │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ useValue │
│ │
│ { provide: API_URL, useValue: '/api' } │
│ │
│ API_URL → '/api' │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ useFactory │
│ │
│ { provide: Config, useFactory: makeConfig } │
│ │
│ Config → makeConfig() │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ useExisting │
│ │
│ { provide: Logging, useExisting: Logger } │
│ │
│ Logging → same instance as Logger │
│ │
└──────────────────────────────────────────────┘
Visual: Injection Context
┌──────────────────────────────────────────────┐
│ ✅ Valid places for inject() │
│ │
│ ─ Constructor │
│ ─ Field initializer │
│ ─ Factory function │
│ ─ Another service │
│ ─ Route guard │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ ❌ Invalid places │
│ │
│ ─ Inside a method (like ngOnInit) │
│ ─ After an await │
│ ─ In a setTimeout │
│ ─ In an event handler │
│ │
│ If you need it later, store it in a field: │
│ │
│ private s = inject(Service); │
│ │
│ ngOnInit() { this.s.doThing(); } │
│ │
└──────────────────────────────────────────────┘
Visual: Service Graph
┌──────────────────────────────────────────────┐
│ APP_CONFIG (InjectionToken) │
│ ▲ │
│ │ │
│ ├─── used by │
│ │ │
│ ┌────┴────────┐ ┌──────────────┐ │
│ │ LoggerService│ │ UserService │ │
│ └────┬─────────┘ └────┬─────────┘ │
│ ▲ │ │
│ │ │ │
│ └───── injected by ─┘ │
│ │
│ │
│ ┌─────────────────────┐ │
│ │ UserListComponent │ │
│ └─────────────────────┘ │
│ │
│ DI resolves the whole graph │
│ │
└──────────────────────────────────────────────┘
Visual: providedIn vs providers
┌──────────────────────────────────────────────┐
│ providedIn: 'root' │
│ │
│ @Injectable({ providedIn: 'root' }) │
│ export class UserService {} │
│ │
│ ✓ Tree-shakable │
│ ✓ No providers array needed │
│ ✓ Modern default │
│ ✓ One instance │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ providers array │
│ │
│ @Component({ providers: [UserService] }) │
│ │
│ ✓ Per-component instance │
│ ✓ No tree-shaking │
│ ✓ Use for scoped state │
│ │
└──────────────────────────────────────────────┘
Visual: Decision Flow
┌──────────────────────────────────────────────┐
│ Is the service shared across the app? │
│ │ │
│ ├── Yes ──► providedIn: 'root' │
│ │ │
│ └── No ──► Is it per-component? │
│ │ │
│ ├── Yes ──► @Component │
│ │ providers │
│ │ │
│ └── No ──► Module providers│
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Need custom implementation? │
│ │ │
│ └── Yes ──► useClass / useFactory │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Injecting a non-class value? │
│ │ │
│ └── Yes ──► InjectionToken │
│ │
└──────────────────────────────────────────────┘
Visual: Testing with DI
┌──────────────────────────────────────────────┐
│ Production │
│ │
│ Component ──► UserService ──► HttpClient │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Test │
│ │
│ TestBed.configureTestingModule({ │
│ providers: [ │
│ { provide: UserService, useValue: mock }│
│ ] │
│ }); │
│ │
│ Component ──► mock UserService │
│ │
│ No HTTP, no side effects │
│ │
└──────────────────────────────────────────────┘
Summary
| Concept | Meaning |
|---|---|
| Service | Class with @Injectable() |
| DI | Framework provides dependencies |
providedIn: 'root' | App-wide singleton |
| Constructor injection | constructor(private s: S) |
inject() | private s = inject(S) |
InjectionToken | Non-class token |
| Provider object | { provide, useClass/Value/Factory } |
| Scope | Where the service is registered |
| Singleton | One instance per scope |
Key takeaways:
- A service is a class with
@Injectable()that holds shared logic or state - Dependency injection provides services to consumers — they never instantiate them
providedIn: 'root'makes a service an app-wide singleton and tree-shakable- Constructor injection —
constructor(private s: S)— is the classic way inject()—private s = inject(S)— is the modern way; must be in an injection context- Scopes — root, module, component — determine instance count
InjectionTokenis for non-class values like config- Provider objects —
useClass,useValue,useFactory,useExisting— swap implementations - Testing mocks services via
TestBedproviders - Inject services, don’t
newthem — that’s the whole point - Use
inject()in field initializers — not in methods or afterawait - Component-scoped services are for state that belongs to a single component instance
Remember: Services hold what components shouldn’t — data access, business logic, shared state. Dependency injection wires them into the app without manual construction. providedIn: 'root' covers most cases; component providers handle per-instance state; injection tokens handle config; provider objects swap implementations. Use inject() for modern code, and never new a service yourself. That’s DI: the framework creates and provides, you just declare what you need.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!