Skip to main content

Angular Providers

When developing Angular applications, you'll often hear about "providers." These are a fundamental part of Angular's dependency injection system, which is what makes services reusable and testable throughout your application. In this article, we'll explore what providers are, the different types available, and how to use them effectively.

What Are Angular Providers?

Providers in Angular are instructions that tell the dependency injection system how to create or deliver a dependency. They are the mechanism through which Angular knows:

  • What dependencies (services) are available
  • How to create instances of these dependencies
  • Where these dependencies should be available

Think of providers as recipes that Angular uses to cook up instances of services when components or other services need them.

Why Do We Need Providers?

Providers solve several key problems:

  1. Reusability: You write a service once and use it in multiple places
  2. Testability: Services can be mocked or substituted during testing
  3. Configuration: Services can be configured differently based on environment
  4. Singleton Management: Control whether a service is shared or unique per component

Different Types of Providers

Angular offers several ways to define providers. Let's explore each one:

1. Class Provider

The class provider is the most common type. It tells Angular to instantiate a class when a dependency is requested.

typescript
// Basic service class
@Injectable()
export class LoggingService {
log(message: string): void {
console.log(`[LOG]: ${message}`);
}
}

// In a module
@NgModule({
providers: [
LoggingService // This is shorthand for { provide: LoggingService, useClass: LoggingService }
]
})
export class AppModule { }

2. Value Provider

The value provider lets you provide a ready-made object instead of having Angular create one:

typescript
// Define a configuration object
const CONFIG = {
apiUrl: 'https://api.example.com',
timeout: 3000
};

// In a module
@NgModule({
providers: [
{ provide: 'APP_CONFIG', useValue: CONFIG }
]
})
export class AppModule { }

To use a value provider with a string token, you'll need to inject it using @Inject:

typescript
import { Component, Inject } from '@angular/core';

@Component({
selector: 'app-root',
template: `<h1>API URL: {{config.apiUrl}}</h1>`
})
export class AppComponent {
constructor(@Inject('APP_CONFIG') public config: any) { }
}

3. Factory Provider

The factory provider allows you to create a service using a factory function, which is useful when you need more complex logic for creating a dependency:

typescript
// A service that might need complex initialization
@Injectable()
export class PaymentService {
constructor(private apiKey: string) { }

processPayment(amount: number): void {
console.log(`Processing $${amount} payment with key: ${this.apiKey}`);
}
}

// Factory function to create the service
export function paymentServiceFactory() {
const apiKey = getApiKeyFromSecureStorage(); // Some logic to get API key
return new PaymentService(apiKey);
}

// In a module
@NgModule({
providers: [
{
provide: PaymentService,
useFactory: paymentServiceFactory
}
]
})
export class AppModule { }

4. Existing Provider

The existing provider lets you alias a provider, so that injecting one token gives you the same instance as another:

typescript
@NgModule({
providers: [
LoggingService,
{ provide: 'ConsoleLogger', useExisting: LoggingService }
]
})
export class AppModule { }

Provider Scopes in Angular

Where you register a provider determines its scope and lifetime:

Module-Level Providers

When registered in a module's providers array, a service is available application-wide and is a singleton:

typescript
@NgModule({
imports: [...],
declarations: [...],
providers: [UserService] // 👈 Available throughout the application
})
export class AppModule { }

Component-Level Providers

When registered in a component's providers array, a new instance is created for that component and its children:

typescript
@Component({
selector: 'app-user-dashboard',
templateUrl: './user-dashboard.component.html',
providers: [UserService] // 👈 New instance for this component tree only
})
export class UserDashboardComponent { }

Injectable Configuration

Starting with Angular 6, you can also configure providers directly in the @Injectable decorator:

typescript
@Injectable({
providedIn: 'root' // 👈 Available application-wide as a singleton
})
export class UserService { }

This approach is preferred for most services as it enables tree-shaking (unused services won't be included in the bundle).

Practical Example: Building a Multi-Level Service System

Let's create a more complete example showing how providers work together. We'll build a logging system with different levels of detail:

First, let's define our logger interface and services:

typescript
// logger.interface.ts
export interface Logger {
info(message: string): void;
error(message: string): void;
}

// basic-logger.service.ts
@Injectable({
providedIn: 'root'
})
export class BasicLoggerService implements Logger {
info(message: string): void {
console.log(`[INFO]: ${message}`);
}

error(message: string): void {
console.error(`[ERROR]: ${message}`);
}
}

// advanced-logger.service.ts
@Injectable()
export class AdvancedLoggerService implements Logger {
info(message: string): void {
console.log(`[INFO] ${new Date().toISOString()}: ${message}`);
}

error(message: string): void {
console.error(`[ERROR] ${new Date().toISOString()}: ${message}`);
// Could also send errors to a monitoring service
}
}

Now, let's use these in different parts of our application:

typescript
// logging.module.ts
@NgModule({
providers: [
// Create an InjectionToken to avoid conflicts with string tokens
{ provide: 'APP_VERSION', useValue: '1.0.0' },

// Our default logger is BasicLoggerService
{ provide: Logger, useClass: BasicLoggerService }
]
})
export class LoggingModule { }

// admin.module.ts
@NgModule({
imports: [CommonModule],
providers: [
// Override the Logger with AdvancedLoggerService for the admin module
{ provide: Logger, useClass: AdvancedLoggerService }
]
})
export class AdminModule { }

Using our loggers in components:

typescript
// user.component.ts (uses BasicLoggerService)
@Component({
selector: 'app-user',
template: `<h2>User Component</h2>
<button (click)="logMessage()">Log Message</button>`
})
export class UserComponent {
constructor(private logger: Logger) { }

logMessage(): void {
this.logger.info('Button clicked in user component');
// Output: [INFO]: Button clicked in user component
}
}

// admin.component.ts (uses AdvancedLoggerService)
@Component({
selector: 'app-admin',
template: `<h2>Admin Component</h2>
<button (click)="logMessage()">Log Message</button>`
})
export class AdminComponent {
constructor(private logger: Logger, @Inject('APP_VERSION') private version: string) { }

logMessage(): void {
this.logger.info(`Button clicked in admin component (v${this.version})`);
// Output: [INFO] 2023-07-10T15:30:45.123Z: Button clicked in admin component (v1.0.0)
}
}

Common Provider Use Cases and Patterns

1. Configuration Services

Providers are perfect for supplying configuration values to your application:

typescript
// config.service.ts
export interface AppConfig {
apiEndpoint: string;
theme: 'light' | 'dark';
features: string[];
}

export const PROD_CONFIG: AppConfig = {
apiEndpoint: 'https://api.myapp.com/v1',
theme: 'light',
features: ['chat', 'notifications']
};

export const DEV_CONFIG: AppConfig = {
apiEndpoint: 'http://localhost:3000/api',
theme: 'dark',
features: ['chat', 'notifications', 'debug']
};

// In your application module
@NgModule({
providers: [
{ provide: 'APP_CONFIG', useValue: environment.production ? PROD_CONFIG : DEV_CONFIG }
]
})
export class AppModule { }

2. API Service Pattern

Use providers to create reusable API services:

typescript
// user-api.service.ts
@Injectable({
providedIn: 'root'
})
export class UserApiService {
constructor(private http: HttpClient, @Inject('APP_CONFIG') private config: AppConfig) { }

getUsers(): Observable<User[]> {
return this.http.get<User[]>(`${this.config.apiEndpoint}/users`);
}

createUser(user: User): Observable<User> {
return this.http.post<User>(`${this.config.apiEndpoint}/users`, user);
}
}

3. Feature Toggling

Use providers to implement feature flags:

typescript
// feature.service.ts
@Injectable({
providedIn: 'root'
})
export class FeatureService {
constructor(@Inject('APP_CONFIG') private config: AppConfig) { }

isFeatureEnabled(featureName: string): boolean {
return this.config.features.includes(featureName);
}
}

// In a component
@Component({
template: `
<div *ngIf="featureService.isFeatureEnabled('chat')">
<app-chat></app-chat>
</div>
`
})
export class HomeComponent {
constructor(public featureService: FeatureService) { }
}

Summary

Providers are a powerful mechanism in Angular that enables the dependency injection system to provide instances of services or values to components and other services. We've covered:

  • Different types of providers (class, value, factory, existing)
  • Provider scopes at different levels (root, module, component)
  • Practical examples of how to use providers effectively
  • Common patterns and use cases for providers

By mastering providers, you gain fine-grained control over how dependencies are created and shared across your Angular application. This leads to more modular, testable, and maintainable code.

Additional Resources

Exercises

  1. Create a service with two implementations (basic and advanced) and use providers to make different parts of your app use different implementations.
  2. Implement a factory provider that creates a service based on the user's browser type or screen size.
  3. Create a configuration service that loads different settings based on the environment (development vs production).
  4. Build a simple logging service that can be overridden at the component level to output logs differently for specific components.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)