Comprehensive Rules for building, organizing, and testing Angular applications that rely on Dependency Injection (DI).
You're spending too much time debugging provider configuration issues, wrestling with circular dependencies, and fighting test setup complexity. Angular's DI system is powerful, but without structured approaches, it becomes a productivity bottleneck instead of an accelerator.
Every Angular developer has been there: NullInjectorError appears in production, circular dependency warnings flood your console, or you're injecting the same service constructor dependencies across dozens of components. These aren't just minor annoyances—they're symptoms of an unstructured approach to dependency injection that costs hours of debugging time weekly.
The real cost isn't just the immediate debugging time. It's the compound effect: slower feature development, brittle tests that break when you refactor, and architecture debt that makes every change more complex than it should be.
These Cursor Rules transform Angular's DI system from a configuration challenge into an architectural advantage. Instead of treating dependency injection as boilerplate you have to manage, you'll build applications where the DI graph becomes your application's structural foundation.
The key insight: treat DI as architecture, not configuration. When your dependency graph reflects your application's actual feature boundaries and data flows, everything else becomes simpler—testing, debugging, performance optimization, and feature development.
// Before: Scattered, unclear dependencies
@Component({
providers: [SomeService, AnotherService] // Why here?
})
export class TodoComponent {
constructor(
private todoService: TodoService,
private authService: AuthService,
private loggerService: LoggerService,
private storageService: StorageService
) {} // Constructor bloat
}
// After: Clean, intentional architecture
@Component({...})
export class TodoComponent {
private readonly todoService = inject(TodoService);
private readonly storage = inject(STORAGE_TOKEN, { optional: true }) ?? localStorage;
// Clear intent, easy to test, tree-shakable
}
Stop writing complex TestBed configurations for every component test. The rules establish consistent provider patterns that make mocking predictable:
// One-line test setup instead of 15-line provider arrays
TestBed.configureTestingModule({
providers: [
{ provide: TodoService, useClass: MockTodoService },
{ provide: API_BASE_URL, useValue: 'http://test-api' }
]
});
The "fail fast" principle catches provider misconfiguration at startup, not when users click buttons. Your development server will refuse to start with broken DI instead of failing silently in specific user flows.
Services live next to the features that use them. No more hunting through a sprawling shared/services directory when you need to modify feature-specific logic:
src/app/features/todo/
├── services/todo.service.ts
├── tokens/storage.token.ts
└── providers/todo.providers.ts
Tree-shakable providers and strategic lazy loading mean you only ship the code users actually need. The rules prevent common mistakes like providing singletons at component level that multiply instances unnecessarily.
Before: NullInjectorError appears. You spend 30 minutes tracing through module imports and provider arrays, checking if services are provided at the right level.
After: Explicit injection tokens and systematic provider declaration patterns mean errors point directly to the misconfigured dependency. Stack traces become actionable.
Before: Copy service setup from existing features, hope you got the provider configuration right, discover issues in testing.
After: Consistent patterns mean new services follow established conventions. The rules guide provider placement and factory configuration automatically.
Before: Changing a service's dependencies requires updating constructors, provider configs, and test mocks across multiple files.
After: Clean injection patterns and systematic mocking strategies mean refactoring touches only the files that actually change behavior.
Add the complete rule set to your .cursorrules file. The configuration includes TypeScript strict mode enforcement, naming conventions, and architectural patterns.
Restructure your services directory to match the feature-first organization:
# Existing structure
src/app/shared/services/
├── todo.service.ts
├── auth.service.ts
└── storage.service.ts
# New structure
src/app/features/todo/
├── services/todo.service.ts
├── tokens/storage.token.ts
└── todo.module.ts
Convert your existing services to use the inject() function pattern:
// Replace this pattern
constructor(
private todoService: TodoService,
private http: HttpClient
) {}
// With this pattern
private readonly todoService = inject(TodoService);
private readonly http = inject(HttpClient);
Create injection tokens for non-class dependencies following the established naming pattern:
// tokens/api-config.token.ts
export const API_BASE_URL = new InjectionToken<string>('API_BASE_URL');
export const FEATURE_FLAGS = new InjectionToken<FeatureFlags>('FEATURE_FLAGS');
Set up factory providers for runtime configuration using the documented patterns:
providers: [
{
provide: AuthConfig,
useFactory: (env: Environment) => new AuthConfig(env.apiKey),
deps: [ENVIRONMENT_TOKEN]
}
]
The biggest impact comes from consistency. When every developer on your team follows the same DI patterns, code reviews focus on business logic instead of architectural discussions. New team members contribute effectively within days instead of weeks because the patterns are systematic and documented.
Your Angular applications become maintainable by default. Feature development accelerates because the architectural decisions are already made. Testing becomes routine because the mocking patterns are established.
Ready to implement structured dependency injection? Copy the rules, refactor one feature module as a proof of concept, and experience the difference systematic DI architecture makes to your Angular development workflow.
You are an expert in Angular 17+, TypeScript 5.x, RxJS 7.x, Jest, Cypress, ESLint, and Prettier.
Key Principles
- Treat Dependency Injection (DI) as the backbone of application architecture.
- Prefer composability (inject, providers) over inheritance.
- Keep providers closest to where they are consumed (feature-first organisation).
- Make services stateless unless they are true singletons.
- Explicit > implicit: inject tokens, do not rely on global objects.
- Fail fast: detect provider mis-configuration at start-up or first injection.
- Code must remain tree-shakable; never reference a provider in a side effect.
TypeScript
- Enable the full strict compiler flag set (strict, noImplicitOverride, exactOptionalPropertyTypes, strictNullChecks).
- Use "private readonly" for injected constructor parameters.
- Prefer readonly properties + pure functions; side-effects only in effects/services.
- Use enums only for closed sets of values; otherwise use string literal union types.
- Never use the @ts-ignore directive; fix the types.
- File naming: kebab-case; suffix .service.ts, .token.ts, .provider.ts.
Error Handling & Validation
- Guard clauses first. Return or throw early; leave the happy path last.
- Wrap third-party injections in an Adapter service to isolate failures.
- Use optional() flag in inject() when a dependency is optional; supply a sane default.
```ts
const logger = inject(LOGGER_TOKEN, { optional: true, self: true }) ?? console;
```
- For asynchronous init, use APP_INITIALIZER and return a Promise that rejects on failure so Angular blocks bootstrapping.
- When a provider cannot be configured (e.g., missing env variable) throw an Error inside the factory so the injector exposes a clear stack trace.
Angular Dependency Injection (Framework-Specific Rules)
- Singleton services: `@Injectable({ providedIn: 'root' })`.
- Feature-scoped services: `@Injectable({ providedIn: 'any' })` or declare in a feature module’s `providers` array.
- Component-level providers only when you explicitly need a new instance per component tree.
- Non-class deps: create an `InjectionToken<T>` and provide via `useValue` or `useFactory`.
```ts
export const API_BASE_URL = new InjectionToken<string>('API_BASE_URL');
providers: [{ provide: API_BASE_URL, useValue: environment.apiBaseUrl }]
```
- Replace constructor injection inside class methods by the signal-safe function `inject()`:
```ts
@Component({...})
export class TodoComponent {
readonly todoService = inject(TodoService);
}
```
- Prefer `providedIn` + tree-shakable providers over NgModule `providers` unless you need multi-providers.
- Multi-token pattern for extensibility:
```ts
export const HTTP_INTERCEPTORS = new InjectionToken<HttpInterceptor[]>('HttpInterceptors');
providers: [{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }]
```
- Do not inject directly into pipes; instead wrap logic in a service.
Provider Declaration Patterns
- `useClass` – default; keep constructor thin.
- `useFactory` – for runtime values; inject inside the factory when additional deps are needed.
```ts
providers: [
{
provide: AuthConfig,
useFactory: (env: Env) => new AuthConfig(env.apiKey),
deps: [ENV_TOKEN]
}
]
```
- `useExisting` – de-duplicate service instances.
- `useValue` – immutable objects only; never large mutable state.
Testing
- Always provide mocks/stubs in `TestBed.configureTestingModule`.
```ts
TestBed.configureTestingModule({
providers: [
{ provide: TodoService, useClass: FakeTodoService },
{ provide: API_BASE_URL, useValue: 'http://localhost' }
]
});
```
- Use `spyOn` & `jest.fn()` on injected mocks to assert interactions.
- End-to-end: use dependency injection to swap real back-end for in-memory API.
Performance
- Lazy-load feature modules and provide their services there to prevent loading unused providers.
- Avoid providing services at component level in lists/tables; it multiplies instances.
- Memoise expensive factories; inject `NgZone` only if you need to escape zone.js.
Security
- Never inject DOM directly; use `Renderer2` or `DomSanitizer`.
- Sanitize all dynamic HTML before rendering.
- Store tokens/credentials only in dedicated `StorageService`; never in a component.
Directory Structure Example
```
src/
app/
features/
todo/
tokens/
api-base-url.token.ts
services/
todo.service.ts
storage.service.ts
components/
todo-page/
todo-page.component.ts
interceptors/
auth.interceptor.ts
todo.module.ts
```
Common Pitfalls & How to Avoid Them
- "NullInjectorError": avoid circular dependencies; extract interfaces.
- Accidentally creating multiple singletons: check that the service is not listed in a component/provider.
- Over-injecting: prefer passing data via inputs when it’s not a cross-cutting concern.
Lint Rules to Enforce
- eslint-plugin-rxjs: enforce `takeUntilDestroyed()` on all subscriptions.
- @angular-eslint/no-outputs-metadata-property: forbid outputs on services, DI only.
Cheat-Sheet
| Need | Provider Syntax |
|--------------------------------|----------------------------------------------------------|
| Immutable config string | `{ provide: TOKEN, useValue: 'foo' }` |
| Runtime object needing deps | `useFactory`, inject inside factory |
| Interface implementation swap | `useClass` in test or runtime |
| Combine multiple hooks | `multi: true` tokens |
| Alias existing service | `{ provide: Alias, useExisting: Real }` |