Angular 126 🅰️ Migrating from AngularJS
Migrating from AngularJS to modern Angular is a significant architectural shift. AngularJS (1.x) reached its official end of life on December 31, 2021, which means it no longer receives security patches, bug fixes, or official support. The migration is not a simple version upgrade; it involves moving from a scope-based, controller-driven model to a component-based architecture with TypeScript, dependency injection, and a completely different change detection system.
The Angular team’s recommended approach for large applications is the hybrid migration using the @angular/upgrade (ngUpgrade) library. This allows both frameworks to run side-by-side in the same application, enabling you to migrate feature by feature without a risky “big bang” rewrite. You can deliver new features in Angular while gradually refactoring legacy code, keeping the application functional at every stage.
This chapter covers the reasons to migrate, the hybrid approach with ngUpgrade, the migration order for services and components, the key technical conversions, routing strategies, a risk-based priority framework, common pitfalls, and the patterns that make the migration successful.
Key point: Migrate incrementally using ngUpgrade, which runs AngularJS and Angular side by side. Migrate shared services before components. Use a wrapper router to handle both routing systems. Prioritize by risk: revenue-critical and customer-facing modules first, then modules touching sensitive data, then internal tools. Audit for dead code before migrating low-traffic features.
Why migrate from AngularJS
The security problem. AngularJS no longer receives patches. Known vulnerabilities remain unaddressed, and any module that handles sensitive data carries compliance exposure. Frameworks in active development receive security fixes; AngularJS does not.
The browser problem. Browser updates can break AngularJS features without warning. Modern browser APIs change, and deprecated behaviors are removed. An unmaintained framework cannot adapt.
The ecosystem problem. Third-party libraries are dropping AngularJS support as their maintainers move to modern frameworks. Dependencies become harder to update, and the ecosystem shrinks over time.
The talent problem. Developers increasingly avoid AngularJS projects because the skills do not transfer to modern frameworks. Hiring and retention become harder as the pool of AngularJS developers shrinks.
The maintainability problem. The scope-based model, implicit dependency injection, and lack of static typing make large AngularJS applications difficult to refactor. Modern Angular’s TypeScript, explicit DI, and component model make the codebase more maintainable.
a. The hybrid migration approach
The hybrid approach runs both frameworks in the same application. The UpgradeModule bridges dependency injection, the DOM, and change detection between AngularJS and Angular.
The setup requires a new Angular project to serve as the host. The main.ts imports the UpgradeModule and overrides ngDoBootstrap to manually bootstrap the AngularJS module:
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { UpgradeModule } from '@angular/upgrade/static';
import { AppModule } from './app/app.module';
import { downgradeInjectable } from '@angular/upgrade/static';
declare const angular: any;
platformBrowserDynamic()
.bootstrapModule(AppModule)
.then((platformRef) => {
const upgrade = platformRef.injector.get(UpgradeModule);
upgrade.bootstrap(document.body, ['myLegacyApp'], { strictDi: true });
})
.catch((err) => console.error(err));
The bootstrap call starts the AngularJS module inside the Angular application. From this point, both frameworks are running, and the bridge allows them to share services and components.
Every element in the DOM is owned by exactly one framework. When an Angular component is used inside an AngularJS template, a boundary is crossed, and the AngularJS framework ignores that element. The bridge handles the transitions.
b. Migrating services first
A critical insight from real migrations is that service migration usually matters more than component migration. Migrating shared services first centralizes ownership, reduces duplicated state, simplifies dependency injection boundaries, and improves long-term modular architecture. A clean UserService migration often delivers more architectural value than converting dozens of presentational components.
An AngularJS factory:
angular.module('myApp').factory('userService', ['$http', function($http) {
return {
getUser: function(id) {
return $http.get('/api/users/' + id);
}
};
}]);
The modern Angular equivalent:
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class UserService {
private readonly http = inject(HttpClient);
getUser(id: string): Observable<User> {
return this.http.get<User>(`/api/users/${id}`);
}
}
The downgradeInjectable function makes the Angular service available to AngularJS code during the transition:
angular.module('myApp').factory('userService', downgradeInjectable(UserService));
This allows legacy AngularJS controllers to use the new service while it is being migrated.
c. Migrating components
Components are migrated by feature boundary. Simple Angular components move before highly coupled directives, but teams should migrate at least one representative “ugly” workflow early — one with custom directives, async callbacks, route guards, and third-party integrations — to avoid dangerously optimistic migration plans.
An AngularJS component:
angular.module('myApp').component('userCard', {
bindings: { user: '<' },
controller: function() {
this.getDisplayName = function() {
return this.user.firstName + ' ' + this.user.lastName;
};
},
template: '<div>{{$ctrl.getDisplayName()}}</div>'
});
The modern Angular equivalent:
import { Component, input, computed } from '@angular/core';
@Component({
selector: 'app-user-card',
template: `<div>{{ displayName() }}</div>`,
})
export class UserCardComponent {
readonly user = input.required<User>();
readonly displayName = computed(
() => `${this.user().firstName} ${this.user().lastName}`
);
}
The downgradeComponent function makes the Angular component available to AngularJS templates:
angular.module('myApp').directive(
'userCard',
downgradeComponent({ component: UserCardComponent })
);
This creates a wrapper that makes the Angular component look like a standard AngularJS directive.
d. Key technical conversions
| AngularJS | Modern Angular |
|---|---|
$scope | Component properties, signals |
.controller() | @Component with @Input() / @Output() |
.factory() / .service() | @Injectable({ providedIn: 'root' }) |
$http | HttpClient with inject() |
$q | Promise / firstValueFrom() |
ng-repeat | @for control flow |
ng-if | @if control flow |
$routeProvider | provideRouter() |
link function | ngOnInit / ngOnChanges |
$watch | computed() / effect() |
$timeout | setTimeout or RxJS timer |
| Two-way binding | [(ngModel)] or signals |
The $scope model is replaced by component properties. In AngularJS, $scope is a shared object that controllers and templates read and write. In Angular, the component class holds the state, and the template binds to it. Signals make the reactive parts explicit.
The $watch model is replaced by computed() and effect(). AngularJS watches expressions and fires listeners when they change. Angular’s signals track dependencies automatically, and computed derives values from them.
e. Routing strategy
Routing is often more complex than teams expect. Nested AngularJS router configurations often hide shared permissions, implicit redirects, inherited layouts, and global state assumptions.
A common pattern is a wrapper router service that handles both UIRouter’s state-based navigation and Angular’s URL-based routing. The wrapper intercepts navigation events, translates between the two routing models, and ensures that the correct framework handles each route.
@Injectable({ providedIn: 'root' })
export class HybridRouterService {
private readonly router = inject(Router);
navigate(path: string): void {
// Check if the path belongs to the Angular router
// If not, delegate to AngularJS $state
}
}
Migrate route groups rather than isolated screens when the AngularJS router structure itself represents the behavioral boundary. A group of routes that share a layout, guards, and resolvers should migrate together, because splitting them creates a period where two routing systems handle related paths.
f. Risk-based migration priority
Not all parts of the application carry the same risk. A tiered approach protects what matters most.
Tier 1: Revenue-critical and customer-facing modules. Checkout flows, payment processing, and user dashboards. These have the highest direct impact on revenue and customer experience. Migrate them first because a bug here costs money.
Tier 2: Modules that touch sensitive data or external services. Authentication systems, API integrations, and data-handling modules. These process sensitive information and connect to external services, creating direct compliance exposure. Migrate them second because a bug here is a security risk.
Tier 3: Internal tools and low-traffic features. Admin panels, reporting dashboards, and low-traffic features can wait. Before migrating this tier, conduct a dead code audit. Many organizations discover that 20–30% of their AngularJS codebase serves no current business purpose, and migrating it is wasted effort.
Complete Example Session
// ============================================
// PART 1: BOOTSTRAP THE HYBRID APP
// ============================================
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { UpgradeModule } from '@angular/upgrade/static';
import { AppModule } from './app/app.module';
platformBrowserDynamic()
.bootstrapModule(AppModule)
.then((platformRef) => {
const upgrade = platformRef.injector.get(UpgradeModule);
upgrade.bootstrap(document.body, ['myLegacyApp'], { strictDi: true });
})
.catch((err) => console.error(err));
// ============================================
// PART 2: UPGRADE MODULE SETUP
// ============================================
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { UpgradeModule } from '@angular/upgrade/static';
import { AppComponent } from './app.component';
@NgModule({
imports: [BrowserModule, HttpClientModule, UpgradeModule],
declarations: [AppComponent],
bootstrap: [AppComponent],
})
export class AppModule {
constructor(private upgrade: UpgradeModule) {}
ngDoBootstrap() {
// Bootstrap is handled in main.ts
}
}
// ============================================
// PART 3: MODERN SERVICE
// ============================================
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class UserService {
private readonly http = inject(HttpClient);
getUser(id: string): Observable<User> {
return this.http.get<User>(`/api/users/${id}`);
}
}
// ============================================
// PART 4: DOWNGRADE SERVICE FOR ANGULARJS
// ============================================
import { downgradeInjectable } from '@angular/upgrade/static';
declare const angular: any;
angular.module('myLegacyApp')
.factory('userService', downgradeInjectable(UserService));
// ============================================
// PART 5: MODERN COMPONENT
// ============================================
import { Component, input, computed } from '@angular/core';
@Component({
selector: 'app-user-card',
template: `<div>{{ displayName() }}</div>`,
})
export class UserCardComponent {
readonly user = input.required<User>();
readonly displayName = computed(
() => `${this.user().firstName} ${this.user().lastName}`
);
}
// ============================================
// PART 6: DOWNGRADE COMPONENT FOR ANGULARJS
// ============================================
import { downgradeComponent } from '@angular/upgrade/static';
angular.module('myLegacyApp').directive(
'userCard',
downgradeComponent({ component: UserCardComponent })
);
// ============================================
// PART 7: UPGRADE ANGULARJS SERVICE FOR ANGULAR
// ============================================
import { Injectable, Inject } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class LegacyDataService {
constructor(@Inject('legacyData') private legacyData: any) {}
getData() {
return this.legacyData.fetch();
}
}
// In the module setup:
// providers: [
// { provide: 'legacyData', useFactory: ($injector: any) =>
// $injector.get('legacyData'), deps: ['$injector'] }
// ]
// ============================================
// PART 8: HYBRID ROUTER SERVICE
// ============================================
import { Injectable, inject } from '@angular/core';
import { Router } from '@angular/router';
@Injectable({ providedIn: 'root' })
export class HybridRouterService {
private readonly router = inject(Router);
navigate(path: string): void {
// Check if the path belongs to the Angular router
// If not, delegate to AngularJS $state
if (this.isAngularRoute(path)) {
this.router.navigateByUrl(path);
} else {
// Delegate to AngularJS
this.legacyNavigate(path);
}
}
private isAngularRoute(path: string): boolean {
return path.startsWith('/new/');
}
private legacyNavigate(path: string): void {
// Use AngularJS $state or $location
}
}
// ============================================
// PART 9: MIGRATE A SERVICE
// ============================================
// Before: AngularJS factory
// angular.module('myApp').factory('apiService', ['$http', function($http) {
// return { fetch: function(url) { return $http.get(url); } };
// }]);
// After: Angular service
@Injectable({ providedIn: 'root' })
export class ApiService {
private readonly http = inject(HttpClient);
fetch(url: string): Observable<unknown> {
return this.http.get(url);
}
}
// ============================================
// PART 10: CLEANUP AFTER MIGRATION
// ============================================
// 1. Remove ngUpgrade
// 2. Remove AngularJS module and dependencies
// 3. Remove downgraded wrappers
// 4. Remove the hybrid bootstrap
// 5. Update the build to include only Angular
// 6. Verify bundle size reduction
These ten parts cover bootstrapping the hybrid app, the upgrade module setup, a modern service, downgrading the service, a modern component, downgrading the component, upgrading an AngularJS service, the hybrid router, a service migration example, and cleanup.
Quick Reference
Migration Approach
| Approach | Description |
|---|---|
| Big bang | Rewrite everything at once |
| Hybrid | Both frameworks run side by side |
| Recommended | Hybrid with ngUpgrade |
Migration Order
| Priority | What to migrate |
|---|---|
| 1 | Shared services |
| 2 | Utilities and state |
| 3 | Simple components |
| 4 | Complex directives |
| 5 | Route groups |
Risk Tiers
| Tier | Modules | Reason |
|---|---|---|
| 1 | Revenue-critical, customer-facing | Direct business impact |
| 2 | Sensitive data, external services | Compliance exposure |
| 3 | Internal tools, low-traffic | Lower priority |
Key Conversions
| AngularJS | Angular |
|---|---|
$scope | Component properties, signals |
.controller() | @Component |
.factory() / .service() | @Injectable |
$http | HttpClient |
$q | Promise |
ng-repeat | @for |
ng-if | @if |
$routeProvider | provideRouter() |
$watch | computed() |
Bridge Functions
| Function | Purpose |
|---|---|
downgradeComponent | Use Angular component in AngularJS |
downgradeInjectable | Use Angular service in AngularJS |
UpgradeComponent | Use AngularJS component in Angular |
UpgradeModule | Bootstrap the hybrid app |
Testing
| Tool | Purpose |
|---|---|
ng test | Angular tests |
karma | AngularJS tests |
TestBed | Angular testing |
ngUpgrade test utilities | Hybrid tests |
Best Practices
✅ Do This:
// Migrate services first
@Injectable({ providedIn: 'root' })
export class UserService { }
// Use downgradeInjectable for legacy code
angular.module('myApp').factory('userService', downgradeInjectable(UserService));
// Migrate by feature boundary
// One route group at a time
// Audit dead code before migrating
// 20-30% may serve no purpose
// Use a wrapper router
// Handle both routing systems
❌ Don’t Do This:
// Migrate everything at once
// Big bang rewrite is risky // ❌
// Migrate presentational components first
// Services matter more // ❌
// Leave the bridge layer permanently
// Remove ngUpgrade after migration // ❌
// Add new AngularJS services
// New features go Angular-first // ❌
// Skip routing planning
// Routing is the hardest part // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Shared mutable state | $scope sprawl | Use services for shared state |
| DOM-heavy directives | Tightly coupled to jQuery | Redesign, do not translate |
| Dual-framework drift | No governance | New features Angular-first |
| Routing inconsistencies | Two routing systems | Wrapper router |
| Migration scope expands | Hidden coupling | Uncover with dependency analysis |
| Bridge layer permanent | No cleanup phase | Plan removal from the start |
| Performance dips | Both frameworks loaded | Lazy loading, split builds |
Real-World Examples
1. Hybrid Bootstrap
platformBrowserDynamic()
.bootstrapModule(AppModule)
.then((ref) => {
ref.injector.get(UpgradeModule)
.bootstrap(document.body, ['legacyApp'], { strictDi: true });
});
2. Modern Service
@Injectable({ providedIn: 'root' })
export class UserService {
private readonly http = inject(HttpClient);
getUser(id: string) { return this.http.get(`/api/users/${id}`); }
}
3. Downgraded Service
angular.module('legacyApp')
.factory('userService', downgradeInjectable(UserService));
4. Modern Component
@Component({
selector: 'app-user-card',
template: `<div>{{ displayName() }}</div>`,
})
export class UserCardComponent {
readonly user = input.required<User>();
readonly displayName = computed(() => this.user().name);
}
5. Downgraded Component
angular.module('legacyApp').directive(
'userCard',
downgradeComponent({ component: UserCardComponent })
);
6. Wrapper Router
navigate(path: string): void {
if (this.isAngularRoute(path)) {
this.router.navigateByUrl(path);
} else {
this.legacyNavigate(path);
}
}
7. Risk Tier 1 Migration
// Checkout flow migrated first
// Payment processing migrated first
8. Dead Code Audit
# Find unused code before migrating
npx unimported
9. Split Build
{
"configurations": {
"hybrid": {
"outputPath": "dist/hybrid"
},
"angular-only": {
"outputPath": "dist/angular"
}
}
}
10. Cleanup
// Remove ngUpgrade after migration
// Remove AngularJS module
// Remove downgraded wrappers
Visual
Hybrid Migration Architecture
┌──────────────────────────────────────────────────────────────┐
│ MAIN.TS │
│ └── bootstrapModule(AppModule) │
│ └── UpgradeModule.bootstrap('legacyApp') │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ ANGULARJS APP │ │
│ │ ┌──────────────────────────────────────────────────┐ │ │
│ │ │ Legacy Controller │ │ │
│ │ │ ├── uses downgraded Angular service │ │ │
│ │ │ └── renders downgraded Angular component │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ ├────────────────────────────────────────────────────────┤ │
│ │ ANGULAR APP │ │
│ │ ┌──────────────────────────────────────────────────┐ │ │
│ │ │ Modern Component │ │ │
│ │ │ ├── uses upgraded AngularJS service │ │ │
│ │ │ └── renders AngularJS component │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ Both frameworks run side by side. │
└──────────────────────────────────────────────────────────────┘
Migration Timeline
┌──────────────────────────────────────────────────────────────┐
│ PHASE 1: SETUP (1-2 weeks) │
│ └── Install Angular CLI, hybrid app, build tools │
│ │
│ PHASE 2: INFRASTRUCTURE (2-4 weeks) │
│ └── Migrate services, utilities, routing, shared components │
│ │
│ PHASE 3: FEATURE MIGRATION (varies) │
│ └── Feature by feature, test, deploy incrementally │
│ │
│ PHASE 4: CLEANUP (1-2 weeks) │
│ └── Remove AngularJS, ngUpgrade, optimize bundle │
└──────────────────────────────────────────────────────────────┘
Risk-Based Priority
┌──────────────────────────────────────────────────────────────┐
│ TIER 1: Revenue-critical, customer-facing │
│ └── Checkout, payment, dashboards │
│ │
│ TIER 2: Sensitive data, external services │
│ └── Auth, API integrations, data handling │
│ │
│ TIER 3: Internal tools, low-traffic │
│ └── Admin panels, reports (audit for dead code first) │
└──────────────────────────────────────────────────────────────┘
Bridge Functions
┌──────────────────────────────────────────────────────────────┐
│ ANGULARJS → ANGULAR: │
│ ├── UpgradeComponent │
│ │ └── Use AngularJS component in Angular │
│ └── UpgradeModule │
│ └── Bootstrap the hybrid app │
│ │
│ ANGULAR → ANGULARJS: │
│ ├── downgradeComponent │
│ │ └── Use Angular component in AngularJS │
│ └── downgradeInjectable │
│ └── Use Angular service in AngularJS │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| AngularJS EOL | December 31, 2021 |
| Recommended approach | Hybrid migration with ngUpgrade |
| Migration order | Services first, then components |
| Routing strategy | Wrapper router for both systems |
| Priority framework | Revenue-critical, sensitive, internal |
| Dead code audit | 20–30% may be unused |
| Bridge functions | downgradeComponent, downgradeInjectable |
| Setup | UpgradeModule.bootstrap |
| Cleanup | Remove AngularJS and ngUpgrade |
| Timeline | Setup, infrastructure, features, cleanup |
Key takeaways:
- AngularJS is end of life. It no longer receives security patches or bug fixes, and browser updates can break it without warning. Migration is a security and maintainability necessity.
- The hybrid approach is the recommended path for large applications. ngUpgrade runs both frameworks side by side, allowing incremental migration without a risky big bang rewrite.
- Migrate services before components. Service migration centralizes ownership, reduces duplicated state, and improves the architecture. It often delivers more value than converting presentational components.
- Use
downgradeComponentanddowngradeInjectableto bridge. These functions make Angular components and services usable in AngularJS code. The reverse direction usesUpgradeComponentand the module’s providers. - Routing is the hardest part. A wrapper router handles both UIRouter’s state-based navigation and Angular’s URL-based routing. Migrate route groups, not isolated screens.
- Prioritize by risk. Revenue-critical and customer-facing modules first, then modules touching sensitive data, then internal tools. Audit for dead code before migrating low-traffic features.
- Governance prevents drift. New features go Angular-first, and the bridge layer must not become permanent. Plan the cleanup phase from the beginning.
- AI-assisted tools can accelerate the mechanical conversion. Tools like
ng-migrate-angularjs-aianalyze the codebase and generate Angular equivalents, but architectural decisions still require human judgment.
Remember: Migrating from AngularJS to Angular is a strategic project, not a code conversion. The hybrid approach with ngUpgrade is the recommended path for large applications because it allows incremental migration without downtime. The migration order matters: services first, then components, then routing. The priority matters: revenue-critical modules first, then sensitive data, then internal tools. The governance matters: new features go Angular-first, and the bridge layer is temporary. The cleanup matters: after migration, remove AngularJS, ngUpgrade, and the bridge wrappers. With a phased approach and the right tooling, the migration can be executed without disrupting operations, and the application emerges with a modern architecture, active security support, and a maintainable codebase.
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!