How to Migrate from AngularJS 1.x to Angular

Type: Software Reference Confidence: 0.93 Sources: 8 Verified: 2026-02-22 Freshness: quarterly

TL;DR

Constraints

Quick Reference

AngularJS PatternAngular EquivalentExample
angular.module('app', [])@NgModule({ ... }) or standalone components@Component({ standalone: true, imports: [...] })
$scope / $rootScopeComponent class properties + @Input/@Output@Input() name: string;
.controller('Ctrl', fn)@Component({ ... }) class Ctrl@Component({ selector: 'app-ctrl', template: '...' })
.directive('myDir', fn)@Directive({ selector: '[myDir]' }) or @Component@Directive({ selector: '[highlight]' })
.service('svc', fn) / .factory(...)@Injectable({ providedIn: 'root' })@Injectable({ providedIn: 'root' }) class SvcService
.filter('fmt', fn)@Pipe({ name: 'fmt' })@Pipe({ name: 'fmt' }) class FmtPipe implements PipeTransform
$http.get(url)HttpClient.get(url)this.http.get<Data[]>('/api/data')
$q.defer() / promisesRxJS Observable (preferred) or native Promisethis.http.get(url).pipe(map(res => res.data))
$routeProvider / ui-routerRouterModule.forRoot(routes){ path: 'home', component: HomeComponent }
ng-repeat="item in items"*ngFor="let item of items" (or @for in Angular 17+)@for (item of items; track item.id) { ... }
ng-if="condition"*ngIf="condition" (or @if in Angular 17+)@if (isLoaded) { <app-data /> }
ng-model="value"[(ngModel)]="value" (FormsModule) or reactive forms<input [(ngModel)]="name"> / formControlName="name"
$broadcast / $emitEventEmitter, RxJS Subject, or Angular Signals@Output() changed = new EventEmitter<string>();
$watch('expr', fn)RxJS streams, OnChanges, or Angular Signals + effect()effect(() => console.log(this.count()))
$compile(html)($scope)Dynamic components via ViewContainerRefviewRef.createComponent(MyComponent)

Decision Tree

START
|-- Is your AngularJS app < 5,000 LOC?
|   |-- YES -> Consider a full rewrite (faster than hybrid setup overhead)
|   +-- NO v
|-- Is the app on AngularJS 1.5+ with .component() API?
|   |-- YES -> Proceed with ngUpgrade incremental migration [Step 1]
|   +-- NO -> First upgrade to AngularJS 1.8.x and refactor to component architecture [Step 1]
|-- Do you use ui-router extensively?
|   |-- YES -> Install @uirouter/angular-hybrid for parallel routing
|   +-- NO v
|-- Do you need both frameworks' routes simultaneously?
|   |-- YES -> Use setUpLocationSync() from @angular/upgrade/static [Step 5]
|   +-- NO v
|-- Is the team experienced with TypeScript?
|   |-- YES -> Migrate services first (bottom-up), then components
|   +-- NO -> Invest 2-4 weeks in TypeScript training before starting migration
|-- Target Angular version?
|   |-- Angular 19+ -> Use standalone components by default, plan for zoneless (Angular 21+)
|   +-- Angular 17-18 -> Use NgModule or standalone (both supported)
+-- DEFAULT -> Use ngUpgrade hybrid approach, migrate leaf components first, work upward

Step-by-Step Guide

1. Prepare the AngularJS codebase

Refactor the AngularJS app to follow the component-based architecture introduced in AngularJS 1.5. Convert controllers + templates into .component() definitions. Follow the "Rule of One" -- one component per file. If on AngularJS 1.2-1.4, first upgrade to 1.8.x. [src1, src2]

// BEFORE: controller-based (AngularJS 1.x style)
angular.module('app').controller('HeroListCtrl', function($scope, HeroService) {
  $scope.heroes = [];
  HeroService.getAll().then(function(heroes) {
    $scope.heroes = heroes;
  });
});

// AFTER: component-based (AngularJS 1.5+ style -- required for ngUpgrade)
angular.module('app').component('heroList', {
  template: '<div ng-repeat="hero in $ctrl.heroes">{{hero.name}}</div>',
  controller: function(HeroService) {
    this.$onInit = function() {
      HeroService.getAll().then(heroes => this.heroes = heroes);
    };
  }
});

Verify: grep -r "$scope" src/ --include="*.js" | wc -l → expected: 0

2. Set up the Angular project alongside AngularJS

Create a new Angular project using Angular CLI. Install the upgrade package. Configure the build system to compile both AngularJS and Angular code. [src1, src7]

# Create Angular project (if not already existing)
ng new my-app --skip-install
cd my-app

# Install ngUpgrade
npm install @angular/upgrade

# If using ui-router, install the hybrid adapter
npm install @uirouter/angular-hybrid
// src/app/app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { UpgradeModule } from '@angular/upgrade/static';

@NgModule({
  imports: [
    BrowserModule,
    UpgradeModule
  ]
})
export class AppModule {
  constructor(private upgrade: UpgradeModule) {}

  ngDoBootstrap() {
    // Bootstrap AngularJS inside Angular
    this.upgrade.bootstrap(document.body, ['myAngularJSApp'], { strictDi: true });
  }
}

Verify: ng build compiles without errors. The app loads and AngularJS components still render.

3. Downgrade an Angular component for use in AngularJS

Create your first Angular component. Use downgradeComponent to register it as an AngularJS directive so it can be used in AngularJS templates. [src1, src2]

// src/app/hero-detail/hero-detail.component.ts
import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-hero-detail',
  template: `<h3>{{ hero.name }}</h3><p>ID: {{ hero.id }}</p>`
})
export class HeroDetailComponent {
  @Input() hero: { id: number; name: string };
}
// src/app/hero-detail/hero-detail.downgrade.ts
import { downgradeComponent } from '@angular/upgrade/static';
import { HeroDetailComponent } from './hero-detail.component';

// Register as AngularJS directive
angular.module('myAngularJSApp')
  .directive('appHeroDetail', downgradeComponent({
    component: HeroDetailComponent
  }));
<!-- Now usable in any AngularJS template -->
<app-hero-detail [hero]="$ctrl.selectedHero"></app-hero-detail>

Verify: The Angular component renders inside AngularJS templates. Inspect the DOM -- the element should have an ng-version attribute.

4. Migrate services from AngularJS to Angular

Rewrite AngularJS services as Angular @Injectable classes. Downgrade them for backward compatibility with remaining AngularJS components. [src2, src5]

// src/app/services/hero.service.ts -- new Angular service
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

interface Hero {
  id: number;
  name: string;
}

@Injectable({ providedIn: 'root' })
export class HeroService {
  constructor(private http: HttpClient) {}

  getAll(): Observable<Hero[]> {
    return this.http.get<Hero[]>('/api/heroes');
  }

  getById(id: number): Observable<Hero> {
    return this.http.get<Hero>(`/api/heroes/${id}`);
  }
}
// Downgrade for AngularJS consumption
import { downgradeInjectable } from '@angular/upgrade/static';
import { HeroService } from './services/hero.service';

angular.module('myAngularJSApp')
  .factory('heroService', downgradeInjectable(HeroService));

Verify: AngularJS components using the downgraded service still function. Check network tab -- HTTP calls go through Angular's HttpClient.

5. Set up routing coexistence

Configure Angular Router alongside AngularJS routing. Use setUpLocationSync() to keep both routers synchronized. [src1, src2]

// src/app/app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

const routes: Routes = [
  // New Angular routes
  { path: 'dashboard', component: DashboardComponent },
  { path: 'heroes/:id', component: HeroDetailComponent },
  // Catch-all: let AngularJS handle unknown routes
  { path: '**', component: AngularJSRouteComponent }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule {}
// In AppModule -- sync location between frameworks
import { setUpLocationSync } from '@angular/upgrade/static';
import { Router } from '@angular/router';

export class AppModule {
  constructor(private upgrade: UpgradeModule, private router: Router) {}

  ngDoBootstrap() {
    this.upgrade.bootstrap(document.body, ['myAngularJSApp'], { strictDi: true });
    setUpLocationSync(this.upgrade);
  }
}

Verify: Navigate between AngularJS and Angular routes. URL changes reflect in both frameworks. Browser back/forward buttons work correctly.

6. Migrate components bottom-up

Work from leaf components (no child dependencies) upward. For each component: rewrite in Angular, replace the AngularJS directive with a downgraded version, remove the old AngularJS code. Track progress by counting remaining AngularJS controllers. [src2, src3]

# Track migration progress
echo "AngularJS controllers remaining:"
grep -r "\.controller(" src/ --include="*.js" | wc -l

echo "AngularJS components remaining:"
grep -r "\.component(" src/ --include="*.js" | wc -l

echo "Angular components created:"
find src/app -name "*.component.ts" | wc -l

Verify: After each component migration, run the full test suite: ng test && npm run e2e

7. Remove AngularJS and finalize

Once all components and services are migrated, remove ngUpgrade, AngularJS dependencies, and the hybrid bootstrap. Switch to pure Angular bootstrap. For Angular 19+, migrate to standalone bootstrap using bootstrapApplication. [src1, src3]

# Remove AngularJS dependencies
npm uninstall angular @angular/upgrade
# If using ui-router hybrid
npm uninstall @uirouter/angular-hybrid @uirouter/angularjs

# Remove from angular.json -- delete any AngularJS script includes
# Update main.ts to standard Angular bootstrap
// src/main.ts -- clean Angular bootstrap (standalone, Angular 19+)
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';

bootstrapApplication(AppComponent, appConfig)
  .catch(err => console.error(err));

Verify: ng build --configuration production succeeds. No references to angular.module, $scope, or @angular/upgrade in codebase.

Code Examples

TypeScript/Angular: Upgrading an AngularJS Service for Use in Angular

// Input:  An existing AngularJS service registered as angular.module('app').service('userService', ...)
// Output: An Angular provider that wraps the AngularJS service for use in Angular components

// src/app/ajs-upgraded-providers.ts
import { InjectionToken } from '@angular/core';

// Define a token for the AngularJS service
export const USER_SERVICE = new InjectionToken<any>('UserService');

// Factory that extracts the AngularJS service via $injector
export function userServiceFactory(i: any) {
  return i.get('userService');
}

// Provider configuration for Angular module
export const userServiceProvider = {
  provide: USER_SERVICE,
  useFactory: userServiceFactory,
  deps: ['$injector']
};
// src/app/profile/profile.component.ts -- using the upgraded service
import { Component, Inject, OnInit } from '@angular/core';
import { USER_SERVICE } from '../ajs-upgraded-providers';

@Component({
  selector: 'app-profile',
  template: `
    <div *ngIf="user">
      <h2>{{ user.name }}</h2>
      <p>{{ user.email }}</p>
    </div>
  `
})
export class ProfileComponent implements OnInit {
  user: any;

  constructor(@Inject(USER_SERVICE) private userService: any) {}

  ngOnInit() {
    this.userService.getCurrentUser().then((user: any) => {
      this.user = user;
    });
  }
}

TypeScript/Angular: Downgrading an Angular Component with Inputs/Outputs

// Input:  An Angular component with @Input and @Output bindings
// Output: A downgraded AngularJS directive that preserves all bindings

// src/app/search-bar/search-bar.component.ts
import { Component, EventEmitter, Input, Output } from '@angular/core';

@Component({
  selector: 'app-search-bar',
  template: `
    <input [value]="query" (input)="onInput($event)" placeholder="Search...">
    <button (click)="onSearch()">Search</button>
  `
})
export class SearchBarComponent {
  @Input() query: string = '';
  @Output() queryChange = new EventEmitter<string>();
  @Output() search = new EventEmitter<string>();

  onInput(event: Event) {
    this.query = (event.target as HTMLInputElement).value;
    this.queryChange.emit(this.query);
  }

  onSearch() {
    this.search.emit(this.query);
  }
}
// src/app/search-bar/search-bar.downgrade.ts
import { downgradeComponent } from '@angular/upgrade/static';
import { SearchBarComponent } from './search-bar.component';

angular.module('myAngularJSApp')
  .directive('appSearchBar', downgradeComponent({
    component: SearchBarComponent
  }));
<!-- Usage in AngularJS template -- note: kebab-case attributes for Angular bindings -->
<app-search-bar
  [query]="$ctrl.searchQuery"
  (query-change)="$ctrl.searchQuery = $event"
  (search)="$ctrl.doSearch($event)">
</app-search-bar>

JavaScript: Automated Migration Progress Tracker Script

Full script: javascript-automated-migration-progress-tracker-sc.js (56 lines)

// Input:  Project source directory path
// Output: Migration progress report (AngularJS vs Angular component counts)

// scripts/migration-progress.js
const { execSync } = require('child_process');

function countPattern(dir, pattern, ext) {
  try {
    const result = execSync(
      `grep -r "${pattern}" "${dir}" --include="*.${ext}" -l 2>/dev/null | wc -l`,
      { encoding: 'utf-8' }
    );
    return parseInt(result.trim(), 10);
  } catch { return 0; }
}

const srcDir = process.argv[2] || './src';
const report = {
  angularjs: {
    controllers: countPattern(srcDir, '\\.controller(', 'js'),
    directives: countPattern(srcDir, '\\.directive(', 'js'),
    services: countPattern(srcDir, '\\.service(\\|.factory(', 'js'),
    filters: countPattern(srcDir, '\\.filter(', 'js'),
  },
  angular: {
    components: countPattern(srcDir, '@Component', 'ts'),
    directives: countPattern(srcDir, '@Directive', 'ts'),
    services: countPattern(srcDir, '@Injectable', 'ts'),
    pipes: countPattern(srcDir, '@Pipe', 'ts'),
  }
};

const totalAJS = Object.values(report.angularjs).reduce((a, b) => a + b, 0);
const totalNG = Object.values(report.angular).reduce((a, b) => a + b, 0);
const pct = totalNG + totalAJS > 0
  ? Math.round((totalNG / (totalNG + totalAJS)) * 100) : 0;

console.log(`Progress: ${pct}% migrated (${totalNG}/${totalNG + totalAJS})`);

Anti-Patterns

Wrong: Bootstrapping AngularJS manually alongside Angular

// BAD -- manually calling angular.bootstrap creates two separate apps
// that cannot share services or components
platformBrowserDynamic().bootstrapModule(AppModule);
angular.bootstrap(document.body, ['myApp']); // Separate bootstrap!

Correct: Use UpgradeModule to bootstrap AngularJS within Angular

// GOOD -- UpgradeModule creates a single hybrid app with shared DI
@NgModule({
  imports: [BrowserModule, UpgradeModule]
})
export class AppModule {
  constructor(private upgrade: UpgradeModule) {}
  ngDoBootstrap() {
    this.upgrade.bootstrap(document.body, ['myApp'], { strictDi: true });
  }
}

platformBrowserDynamic().bootstrapModule(AppModule);

Wrong: Using $scope in migrated components

// BAD -- $scope creates tight coupling to AngularJS digest cycle
// and cannot be migrated to Angular
angular.module('app').component('userCard', {
  controller: function($scope, UserService) {
    $scope.user = null;
    $scope.$watch('userId', function(id) {
      UserService.get(id).then(function(u) { $scope.user = u; });
    });
  }
});

Correct: Use component bindings and lifecycle hooks

// GOOD -- component bindings map directly to Angular @Input/@Output
angular.module('app').component('userCard', {
  bindings: { userId: '<' },
  controller: function(UserService) {
    this.$onChanges = function(changes) {
      if (changes.userId && changes.userId.currentValue) {
        UserService.get(this.userId).then(u => this.user = u);
      }
    };
  }
});

Wrong: Migrating everything at once (big bang)

# BAD -- rewriting the entire app in one branch
git checkout -b full-angular-rewrite
# ... 6 months of parallel development ...
# Result: massive merge conflicts, feature drift, missed deadlines

Correct: Incremental migration with continuous deployment

# GOOD -- migrate one component at a time, merge to main frequently
git checkout -b migrate/hero-detail-component
# Migrate one component, downgrade for AngularJS compatibility
# Run tests, deploy, merge
git checkout -b migrate/hero-service
# Next component...

Wrong: Duplicating state between frameworks

// BAD -- maintaining separate state in both Angular and AngularJS
// leads to synchronization bugs
// In Angular:
this.cartItems = [...]; // Angular state
// In AngularJS:
$scope.cartItems = [...]; // AngularJS state -- gets out of sync!

Correct: Single source of truth via downgraded/upgraded services

// GOOD -- one service owns the state, shared via downgrade
@Injectable({ providedIn: 'root' })
export class CartService {
  private items = new BehaviorSubject<CartItem[]>([]);
  items$ = this.items.asObservable();

  addItem(item: CartItem) {
    this.items.next([...this.items.value, item]);
  }
}

// Downgrade for AngularJS
angular.module('app').factory('cartService', downgradeInjectable(CartService));

Wrong: Ignoring Zone.js performance in hybrid mode

// BAD -- every AngularJS digest triggers Angular change detection
// and vice versa, creating cascading cycles
// No optimization = 2x-5x slower than either framework alone

Correct: Use OnPush change detection in migrated components

// GOOD -- OnPush prevents unnecessary change detection in hybrid mode
@Component({
  selector: 'app-hero-list',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `<div *ngFor="let hero of heroes$ | async">{{ hero.name }}</div>`
})
export class HeroListComponent {
  heroes$ = this.heroService.getAll();
  constructor(private heroService: HeroService) {}
}

Wrong: Using deprecated .toPromise() for Observable-to-Promise conversion

// BAD -- .toPromise() is deprecated since RxJS 7
const heroes = await this.heroService.getAll().toPromise();

Correct: Use firstValueFrom() or lastValueFrom()

// GOOD -- firstValueFrom() is the modern replacement (RxJS 7+)
import { firstValueFrom } from 'rxjs';

const heroes = await firstValueFrom(this.heroService.getAll());

Common Pitfalls

Diagnostic Commands

# Check AngularJS version
node -e "console.log(require('./node_modules/angular/package.json').version)"

# Check Angular CLI and framework version
ng version

# Count remaining AngularJS artifacts
grep -r "angular\.module\|\.controller(\|\.directive(\|\.factory(\|\.service(" src/ --include="*.js" | wc -l

# Find all $scope references (should be 0 before migration)
grep -rn "\$scope" src/ --include="*.js" --include="*.ts"

# Check for circular dependencies
npx madge --circular src/app

# Verify no AngularJS code remains after migration
grep -r "angular\." src/ --include="*.ts" --include="*.js" | grep -v node_modules | grep -v ".spec."

# Run Angular production build to catch any issues
ng build --configuration production

# Analyze bundle size (check for leftover AngularJS)
npx webpack-bundle-analyzer dist/my-app/stats.json

# Check for deprecated APIs before removing Zone.js (Angular 19+)
grep -rn "NgZone\|onMicrotaskEmpty\|onUnstable\|isStable\|onStable" src/ --include="*.ts"

Version History & Compatibility

VersionStatusngUpgrade SupportMigration Notes
AngularJS 1.8.xEOL (Dec 2021)Required sourceLast AngularJS release. Must be on 1.5+ for component API. [src6]
Angular 2-5EOLFullOriginal ngUpgrade. Requires SystemJS or custom webpack config.
Angular 6-8EOLFullAngular CLI builder. @angular/upgrade/static is the stable API.
Angular 9-12EOLFull (Ivy)Ivy renderer. entryComponents still required until Angular 13.
Angular 13-15EOLFull (Ivy)entryComponents removed. ViewEngine dropped in 13.
Angular 16-17EOLFullStandalone components, signals, new control flow syntax (@if, @for). [src1]
Angular 18EOLFullExperimental zoneless change detection. Signal-based components.
Angular 19LTS (until May 2026)Fullstandalone: true by default. Stable signals API. [src7]
Angular 20LTS (until Nov 2026)FullZoneless stable. Signal-based components mature.
Angular 21ActiveFullZoneless by default. Zone.js removal recommended for new apps.

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
Existing AngularJS app > 5,000 LOC with active developmentApp is < 5,000 LOC or in maintenance-only modeFull rewrite in Angular, React, or Vue
Team has Angular/TypeScript skills or budget to trainTeam has no TypeScript experience and no training budgetConsider React (gentler JS-first learning curve)
Business requires continuous feature delivery during migrationApp can go into feature freeze for 3-6 monthsBig-bang rewrite during freeze period
Shared services and state make incremental approach necessaryFrontend and backend are being rewritten simultaneouslyGreenfield Angular project from scratch
AngularJS security patches are needed (EOL means no patches)Using HeroDevs NES (Never-Ending Support) for AngularJSStay on AngularJS with commercial LTS support
You want to stay in the Angular ecosystem with familiar patternsTeam prefers a completely different paradigmMigrate to React, Vue, or Svelte instead

Important Caveats

Related Units