DEV Community

Cover image for Angular Standalone Migration: A Deep Dive into Dependency Injection Pitfalls & Best Practices
Saurabha Arya
Saurabha Arya

Posted on

Angular Standalone Migration: A Deep Dive into Dependency Injection Pitfalls & Best Practices

πŸš€ Upgrading a large Angular application from v17 to v21 isn’t just a version bump β€” it was an architectural wake-up call.

One of the biggest shifts I had to tackle was moving the app to a fully standalone architecture. That change alone forced me to pause and ask an important question:

πŸ‘‰ β€œDo we actually understand how dependency injection is working in our app?”

As I dug deeper, I realized many DI patterns in the codebase worked by coincidence, not by design. They had survived for years thanks to NgModulesβ€” but in a standalone world, they suddenly felt fragile, confusing, or outright wrong.

πŸ” In this article, I’ll walk through real cases from a production app:

  • what the original code looked like
  • why it worked earlier
  • where it breaks or misleads in standalone Angular
  • and how to fix it cleanly and correctly

If you’re upgrading an existing Angular app or refactoring legacy DI patterns β€” this guide is for you.

⚠️ The migration is still ongoing β€” I’ll be publishing more articles as I continue the upgrade.


1. From NgModule.providers to providedIn: 'root'

❌ Legacy pattern (NgModule era)

This worked because:

  • The module owned the DI scope
  • Services were implicitly singleton within the module

βœ… Standalone replacement

Why this is correct

  • providedIn: 'root' replaces module-level providers
  • Singleton behavior is preserved
  • Tree-shakable and future-proof

Rule:

If a service was previously in Module.providers, it should almost always move to providedIn: 'root'.


2. Services in bootstrapApplication β€” what really belongs there?

❌ What I initially had in main.ts

This works, but it’s not a correct standalone design.

βœ… What SHOULD be in main.ts

What bootstrapApplication is for:

βœ… Framework overrides (ErrorHandler, TitleStrategy, etc.)
βœ… Router setup & strategies
βœ… HttpClient + interceptors
βœ… App-wide configuration tokens
βœ… Platform / framework modules
βœ… Legacy services you cannot modify

What it is NOT for:

❌ API / CRUD services
❌ Business/domain services
❌ Feature logic
❌ Component state services


3. Services with Subject / BehaviorSubject: Root or not?

My use case

  • App loads a master API
  • Stores success/error messages in a BehaviorSubject
  • Multiple components read from it

❌ Legacy misuse

This creates:

  • Multiple instances
  • Multiple subscriptions
  • Hidden bugs

βœ… Correct design

If a service stores state that many parts of the app use, it should live at the root so everyone gets the same data.


4. Services in component providers+ providedIn: 'root'

Yes, Angular allows this β€” but it’s dangerous.

Result

  • Component gets a new instance
  • App gets a different instance

⚠️ For API, auth, socket, or state services β€” this is almost always a bug.


5. Pipes (DatePipe& custom pipes) in providers

❌ What I found

βœ… Correct usage

Or simply inject without providing:

Custom pipes used only in .ts?

πŸ‘‰ They should not be pipes β€” convert them to:

  • utility functions
  • or services

6. Components injected into other components (⚠️ critical)

❌ Legacy anti-pattern I found

This creates a ghost component instance:

  • Not rendered
  • Not part of the DOM
  • Not change-detected

βœ… Correct approaches
Option 1: @ViewChild

Option 2: Shared service (recommended)

Components should NEVER appear in providers.


7. .spec.ts files and providers β€” is that okay?

Yes β€” tests have their own DI world.

This:

  • Is test-scoped
  • Does not affect runtime DI
  • Is perfectly valid

No changes needed during migration.


8. GlobalErrorHandler β€” the exception that belongs in main.ts

Why it must stay in main.ts and not part of a component?
🧠 Angular itself uses ErrorHandler, so it must be registered during app bootstrap, before any component is created.
🌍 Error handling must be truly global β€” errors can occur outside component lifecycles and feature scopes.
πŸ—οΈ Components manage UI, not framework behavior, and global error handling is part of Angular’s core infrastructure.
⚠️ Providing it in components can create multiple instances, leading to inconsistent or missed error handling.


Final Takeaways

  • Standalone Angular removes NgModules, not DI rules
  • providedIn: 'root' replaces most module-level providers
  • bootstrapApplication is for framework wiring only
  • Components must never be used as services
  • Stateful services belong at root unless isolation is intentional

πŸš€ If you’re migrating to standalone Angular or have run into DI-related challenges, feel free to share your questions or experiences in the comments β€” I’d love to discuss them πŸ™‚

Top comments (0)