How to Migrate AngularJS to Modern Angular: The 2026 Enterprise Guide

Migrating a large AngularJS application is not a simple framework replacement. It is an architectural migration involving dependency injection, routing, templates, state management, HTTP behavior, testing, build tooling, authentication, observability, and deployment.

For enterprise systems, the safest approach is usually incremental. Establish a supported Angular baseline, isolate legacy boundaries, introduce modern Angular where appropriate, migrate complete business capabilities, measure the result, and remove AngularJS only after its remaining dependencies have disappeared.

Executive Summary: AngularJS applications become increasingly difficult to maintain when their runtime, dependencies, build system, browser assumptions, and architectural patterns diverge from the supported Angular platform. A successful migration therefore focuses on dependency ownership and business boundaries rather than mechanically converting controllers and templates one file at a time.
  How to Migrate AngularJS to Modern Angular

1. Why AngularJS Migration Is an Architectural Problem

A conventional library upgrade assumes that the application's architectural model remains mostly unchanged. AngularJS to modern Angular migration does not provide that luxury.

AngularJS applications commonly rely on controllers, scopes, directives, filters, factories, providers, AngularJS services, route configuration, watchers, digest-cycle behavior, and third-party AngularJS modules. Modern Angular is built around components, TypeScript, compiled templates, modern dependency injection, standalone components, signals, RxJS, and the Angular CLI.

The dangerous part of migration is not syntax conversion. The dangerous part is changing ownership.

Who owns application state? Who initiates an HTTP request? Which component owns a subscription? Which router controls navigation? Where does authentication state live? Which code is allowed to manipulate the DOM? These questions become much more important when two framework runtimes temporarily coexist.

Production warning: Do not begin by converting every AngularJS controller into an Angular component. That often produces a modern-looking codebase with the same old architectural problems.

2. Establish a Baseline Before Migration

Before changing application code, capture the current production behavior. The baseline becomes your reference point for performance, errors, API traffic, and memory behavior.

Area Capture Purpose
Build Bundle sizes, build duration, warnings Detects build and payload regressions.
Runtime Startup, route transitions, long tasks Shows user-facing performance changes.
Network Request count, failures, payload size Detects duplicated API operations.
Memory Heap snapshots and retained objects Helps identify lifecycle leaks.
Errors Browser and API errors Provides before-and-after comparison.
Tests Unit, integration and E2E coverage Protects business behavior.

Avoid publishing invented performance percentages. A statement such as "Angular reduced memory by 40 percent" is not meaningful without the application size, browser version, device, workload, dataset, build mode, and measurement method.

If you publish benchmark results on your own engineering blog, include the environment and methodology. A reproducible measurement is far more useful than a precise number with no evidence.

Practical rule: Measure the same route, dataset, browser, device class, network profile, and production build configuration before and after migration.

3. Choose the Migration Strategy

There are three broad approaches to an AngularJS migration.

Strategy Advantages Risks Typical Fit
Big-bang rewrite Clean target architecture Large release risk and long migration period Smaller or low-change applications
Hybrid migration Incremental delivery Two frameworks temporarily coexist Large enterprise applications
Feature-shell replacement Clear domain ownership Requires strong routing boundaries Applications with separable domains

For a large enterprise system that cannot tolerate a long feature freeze, an incremental migration is often worth evaluating first. The important point is not that hybrid migration is automatically better. The important point is that migration risk can be distributed across smaller releases.

4. Step-by-Step Migration Plan

STEP 1 Freeze New AngularJS Development

A migration cannot progress if new AngularJS functionality continues to be added at the same rate as migrated code.

Create a simple architectural rule: new business functionality should normally be implemented in the modern Angular boundary. AngularJS should receive only necessary production fixes, security patches, and explicitly approved exceptions.

Useful governance rule: Every new AngularJS exception should have an owner and a reason. Otherwise temporary compatibility decisions tend to become permanent architecture.

STEP 2 Inventory the AngularJS Application

Search for AngularJS modules, controllers, directives, services, filters, providers, global events, timers, DOM manipulation, HTTP interceptors, route configuration, and third-party dependencies.

npm ls --depth=0 npm outdated npx depcheck grep -R "\$scope" src/ grep -R "\$rootScope" src/ grep -R "angular.module" src/ grep -R "\$http" src/ grep -R "\$timeout" src/ grep -R "\$interval" src/

The package commands expose dependency relationships while the searches identify common AngularJS runtime constructs. These searches are not proof that a dependency is removable; dynamically registered modules and runtime-loaded code still need manual verification.

STEP 3 Establish a Supported Angular Toolchain

Choose the target Angular version first, then use the official compatibility matrix to determine the compatible Node.js and TypeScript versions.

node --version npm --version npx ng version

Record the output in your migration documentation. CI and developer environments should use a controlled runtime rather than arbitrary local Node.js versions.

{ "scripts": { "build": "ng build", "test": "ng test --watch=false" } }

Keep the package-lock file under version control and use deterministic dependency installation in CI. The exact Node.js and Angular versions should be selected from the compatibility matrix for the release you are adopting.

STEP 4 Create the Modern Angular Shell

The application shell should own cross-cutting concerns such as routing, global providers, authentication integration, configuration, and global error handling.

import { ApplicationConfig } from '@angular/core'; import { provideRouter } from '@angular/router'; import { provideHttpClient, withInterceptors } from '@angular/common/http'; import { routes } from './app.routes'; import { authInterceptor } from './core/auth.interceptor'; export const appConfig: ApplicationConfig = { providers: [ provideRouter(routes), provideHttpClient( withInterceptors([authInterceptor]) ) ] };

This creates an application-level provider configuration without requiring every new feature to depend on a large NgModule hierarchy.

Keep the root configuration small. If business logic starts accumulating in the application shell, the migration boundary is probably too broad.

STEP 5 Define Business-Capability Boundaries

A useful migration unit is a complete business capability such as Orders, Customers, Reports, Administration, or Billing.

Strong migration boundary: route + components + state + API service + authorization + tests.

Weak migration boundary: three unrelated controllers, a shared service, two directives, and a global utility.

The first approach creates an independently testable unit. The second spreads migration dependencies across the application and makes rollback significantly harder.

STEP 6 Convert Service Contracts

AngularJS services often contain the real dependencies of a feature. Migrating those contracts early makes the component migration easier.

import { Injectable, inject } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; export interface Customer { id: string; displayName: string; status: 'active' | 'inactive'; } @Injectable({ providedIn: 'root' }) export class CustomerService { private readonly http = inject(HttpClient); getCustomer(id: string): Observable<Customer> { const encodedId = encodeURIComponent(id); return this.http.get<Customer>( `/api/customers/${encodedId}` ); } }

The service owns HTTP access while the component owns presentation. URL encoding also prevents an identifier containing reserved characters from changing the intended request path.

Do not automatically create a global state service for every AngularJS service. Some legacy services exist only because of AngularJS architectural conventions.

STEP 7 Convert Controllers Into Focused Components

import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core'; import { Customer, CustomerService } from '../data/customer.service'; @Component({ selector: 'app-customer-profile', changeDetection: ChangeDetectionStrategy.OnPush, template: ` <section> @if (customer(); as value) { <h2>{{ value.displayName }}</h2> <p>Status: {{ value.status }}</p> } @else { <p>Loading customer...</p> } </section> ` }) export class CustomerProfileComponent { private readonly customerService = inject(CustomerService); protected readonly customer = signal<Customer | null>(null); loadCustomer(id: string): void { this.customerService.getCustomer(id).subscribe({ next: value => this.customer.set(value), error: () => this.customer.set(null) }); } }

Signals provide explicit reactive state for values consumed by the component. The component remains responsible for presentation while the service owns data access.

For complex asynchronous workflows, keep the operation as an observable and use appropriate RxJS operators. Do not convert every stream into a signal simply because signals are available.

STEP 8 Replace $scope Communication

AngularJS applications frequently use scope inheritance or global events to communicate between parts of the application.

During migration, classify each communication path:

  • Parent to child: component inputs.
  • Child to parent: component outputs.
  • Route state: route parameters or route data.
  • Server state: dedicated data services.
  • Local UI state: signals or component state.
  • Cross-feature state: explicit state-management architecture.

Avoid creating a single global event bus just to replace $rootScope.$broadcast(). That simply recreates the original coupling under a new API.

STEP 9 Manage RxJS Lifecycles

AngularJS code often used $scope.$on('$destroy') for cleanup. Modern Angular provides lifecycle-aware patterns that should be used for subscriptions and external listeners.

import { Component, DestroyRef, inject } from '@angular/core'; import { interval } from 'rxjs'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ selector: 'app-polling', template: '<p>Polling is active.</p>' }) export class PollingComponent { private readonly destroyRef = inject(DestroyRef); constructor() { interval(10000) .pipe( takeUntilDestroyed(this.destroyRef) ) .subscribe(() => { this.refresh(); }); } private refresh(): void { // Feature-owned refresh operation. } }

The subscription is connected to the component lifecycle. When the component is destroyed, the subscription is terminated instead of continuing indefinitely.

For production polling, also consider retry limits, exponential backoff, visibility state, server load, and cancellation.

STEP 10 Migrate HTTP Interceptors

Authentication behavior frequently lives inside AngularJS interceptors. Reproduce the required security behavior rather than copying the old implementation line by line.

import { HttpInterceptorFn } from '@angular/common/http'; export const authInterceptor: HttpInterceptorFn = ( req, next ) => { const token = sessionStorage.getItem('access_token'); if (!token) { return next(req); } const authenticatedRequest = req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }); return next(authenticatedRequest); };

This example demonstrates the interceptor mechanism. The storage mechanism itself requires a security review.

Security caveat: Do not assume that an AngularJS token-storage pattern remains appropriate for the new application. Review XSS exposure, CSRF protection, token lifetime, refresh behavior, logout invalidation, cookie attributes, and identity-provider requirements.

STEP 11 Migrate Routing by Feature

import { Routes } from '@angular/router'; export const routes: Routes = [ { path: 'customers', loadComponent: () => import('./customers/customer-list.component') .then(m => m.CustomerListComponent) }, { path: 'customers/:id', loadComponent: () => import('./customers/customer-profile.component') .then(m => m.CustomerProfileComponent) }, { path: '', pathMatch: 'full', redirectTo: 'customers' }, { path: '**', redirectTo: 'customers' } ];

Route-level lazy loading creates a useful architectural boundary. A feature can own its components, services, tests, and dependencies without automatically becoming part of the initial application payload.

However, lazy loading is not automatically faster. Excessive chunking can create additional requests and navigation overhead. Measure the resulting application rather than assuming that more chunks are always better.

5. Hybrid AngularJS and Angular Migration

For large applications, Angular's upgrade tooling can support a hybrid period where AngularJS and Angular operate together.

The hybrid phase should be treated as migration infrastructure, not the final architecture.

import { NgModule } from '@angular/core'; import { UpgradeModule } from '@angular/upgrade/static'; @NgModule({ imports: [ UpgradeModule ] }) export class LegacyBridgeModule { }

The exact bootstrap configuration depends on the existing AngularJS bootstrap process, the ownership of routing, and which services or components cross the framework boundary.

Hybrid cost: Two runtimes mean additional dependency graphs, lifecycle boundaries, bootstrap complexity, testing complexity, and developer cognitive load. Define exit criteria for the hybrid layer before introducing it.

6. Signals Versus RxJS During Migration

Modern Angular gives developers more than one reactive primitive. That does not mean an enterprise migration should standardize every piece of state on one technology.

Problem Reasonable Choice Question to Ask
Local UI state Signal Does the state belong only to this component?
Derived state Computed signal Can the value be derived deterministically?
HTTP workflow Observable Do cancellation and stream operators matter?
WebSocket stream RxJS Does data continuously arrive over time?
Cross-feature state Explicit state architecture Who owns writes and synchronization?

The migration goal is not "remove RxJS." The goal is to use each abstraction where its lifecycle and semantics make sense.

7. Performance Engineering

A hybrid migration can temporarily increase JavaScript payload and runtime complexity. AngularJS watchers may continue running while new Angular components are introduced.

Track:

  • Initial JavaScript transfer size.
  • JavaScript parse and execution time.
  • Largest Contentful Paint.
  • Interaction to Next Paint.
  • Long tasks.
  • Route-level network requests.
  • Heap growth during repeated navigation.
  • Detached DOM nodes.
Performance evidence rule: If you publish a benchmark, document the browser, device, build mode, application version, dataset, network profile, and measurement method. Do not present an arbitrary number as an industry benchmark.

For internal engineering, define budgets such as maximum initial JavaScript size, maximum API requests during a route transition, and an acceptable memory-stability threshold after repeated navigation.

8. Terminal Verification

$ node --version v22.x.x $ npm --version 10.x.x $ npx ng version Angular CLI: 22.x.x Node: 22.x.x Package Manager: npm $ npm ci added packages $ npm run build Application bundle generation complete. $ npm test -- --watch=false Executed tests successfully. $ git status --short M src/app/app.config.ts M src/app/app.routes.ts

The exact version numbers depend on the Angular release selected by the project. The important requirement is reproducibility across developer machines and CI.

9. Common Production Pitfalls

Problem 1: Duplicate API Requests

A hybrid route can accidentally initialize both AngularJS and Angular data-loading logic. Both runtimes then call the same endpoint.

Fix: assign one framework ownership of server-state loading and verify the behavior with browser network traces and request correlation IDs.

Problem 2: Memory Growth After Navigation

Common causes include subscriptions, global event listeners, timers, detached DOM nodes, and services retaining component references.

Fix: reproduce the same navigation sequence repeatedly, compare heap snapshots, and inspect retained objects.

Problem 3: Legacy Directive Failure

A directive may depend on AngularJS compilation order, scope inheritance, transclusion, or specific DOM behavior.

Fix: identify the directive's actual responsibility and implement it as a focused Angular component, directive, or service.

Problem 4: Production Authentication Failure

Authentication can fail after deployment because production domains, HTTPS, proxy headers, cookies, CSP, token expiry, or API origins differ from localhost.

Fix: validate the complete authentication flow in a production-like staging environment.

10. Forensic Failure Ledger

The following incidents describe representative migration failure patterns. The traces are examples of what an investigation might show; they should not be presented as telemetry from a specific company unless you actually collected that telemetry.

Incident 1: Resource Leak During Network Degradation

Representative trace:

Error: Unhandled request retry loop at HttpClient.request at CustomerRepository.refresh at PollingService.tick NetworkError: Failed to fetch Retry count exceeded Component state retained after route destruction

Root cause:

A polling operation continued after the owning component was destroyed. Network degradation triggered retries while the application retained objects associated with the old route.

Production patch:

import { DestroyRef, Injectable, inject } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { defer, timer } from 'rxjs'; import { retry, switchMap, takeUntilDestroyed } from 'rxjs/operators'; @Injectable({ providedIn: 'root' }) export class CustomerPollingService { private readonly http = inject(HttpClient); private readonly destroyRef = inject(DestroyRef); start(id: string): void { timer(0, 30000) .pipe( switchMap(() => defer(() => this.http.get( `/api/customers/${encodeURIComponent(id)}` ) ) ), retry({ count: 3, delay: (_error, retryIndex) => timer( Math.min( 1000 * 2 ** retryIndex, 10000 ) ) }), takeUntilDestroyed(this.destroyRef) ) .subscribe(); } }

The important properties are lifecycle ownership, bounded retries, and backoff. Production systems should also consider server-side rate limits and cancellation during application shutdown.

Incident 2: Race Condition During Burst Navigation

Representative trace:

ERROR Error: Stale customer response GET /api/customer/100 GET /api/customer/101 Response: customer=100 Current route: customer=101 Result: customer=100 rendered over customer=101

Root cause:

Two asynchronous requests were allowed to complete independently. The older request returned after navigation had already changed the active customer.

Production patch:

import { Component, inject } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { distinctUntilChanged, switchMap } from 'rxjs/operators'; import { CustomerService } from './customer.service'; @Component({ selector: 'app-customer-route', template: ` <app-customer-profile [customer]="customer$ | async"> </app-customer-profile> ` }) export class CustomerRouteComponent { private readonly route = inject(ActivatedRoute); private readonly customerService = inject(CustomerService); protected readonly customer$ = this.route.paramMap.pipe( distinctUntilChanged( (previous, current) => previous.get('id') === current.get('id') ), switchMap(params => { const id = params.get('id'); if (!id) { throw new Error( 'Customer route requires an id parameter' ); } return this.customerService.getCustomer(id); }) ); }

The route identity controls the stream. When the route changes, the previous request is no longer the owner of the current view.

Incident 3: Memory Thrashing During Sustained Navigation

Representative diagnostic output:

Chrome DevTools Heap Snapshot Navigation cycle: 1 baseline Navigation cycle: 25 +18 MB retained Navigation cycle: 50 +41 MB retained Navigation cycle: 100 +79 MB retained Dominators: Window listeners Legacy controller references Detached DOM nodes RxJS Subscriber instances

Root cause:

A legacy directive registered a global listener and retained references associated with a destroyed controller.

Production patch:

import { Component, DestroyRef, inject } from '@angular/core'; @Component({ selector: 'app-keyboard-shortcuts', template: '<ng-content></ng-content>' }) export class KeyboardShortcutsComponent { private readonly destroyRef = inject(DestroyRef); constructor() { const handler = ( event: KeyboardEvent ): void => { if (event.key === 'Escape') { this.closeDialog(); } }; window.addEventListener( 'keydown', handler ); this.destroyRef.onDestroy(() => { window.removeEventListener( 'keydown', handler ); }); } private closeDialog(): void { // Feature-owned dialog close operation. } }

The correct validation is not simply "memory decreased." Repeatedly create and destroy the same feature and verify that the retained object graph stabilizes.

Incident 4: Stale State After Backend Failover

Representative trace:

HTTP 409 Conflict Request-Id: 8d2f... Resource-Version: 417 Client-Version: 415 Error: Resource version is newer on server.

Root cause:

The frontend treated cached state as authoritative while another backend instance had already accepted a newer version of the resource.

Production patch:

export interface VersionedCustomer { id: string; version: number; displayName: string; } export interface UpdateCustomerRequest { displayName: string; expectedVersion: number; } export class CustomerConflictError extends Error { constructor( public readonly expectedVersion: number, public readonly serverVersion: number ) { super( 'Customer was changed by another request.' ); this.name = 'CustomerConflictError'; } }

The broader fix is an explicit consistency strategy. Depending on the backend, this can involve optimistic concurrency, version checks, cache invalidation, or a server-authoritative refresh after conflicts.

11. Security Audit and Access Control

A framework migration is a good point to reassess security assumptions inherited from the AngularJS application.

Security checklist:
  • Use a currently supported Angular release.
  • Keep production builds compiled and optimized.
  • Avoid dynamic templates from user-controlled input.
  • Audit direct DOM manipulation.
  • Review trusted HTML and bypass APIs.
  • Use an appropriate Content Security Policy.
  • Consider Trusted Types where compatible with the application.
  • Keep backend authorization authoritative.
  • Use least-privilege CI and deployment identities.
  • Rotate credentials and short-lived tokens according to the identity architecture.
  • Use TLS for production service communication.

Container Isolation

The Angular build container should not need production credentials. Build jobs should have only the permissions necessary to retrieve dependencies, compile the application, and publish the resulting artifact.

Least-Privilege Service Accounts

Separate build, artifact publication, deployment, and runtime identities where possible. A CI job that compiles frontend assets does not need unrestricted production administration access.

Token Rotation

The frontend should not be treated as the security authority. Backend services must validate tokens, issuer, audience, expiration, permissions, and other required claims.

mTLS

mTLS is primarily relevant to service-to-service communication. If the Angular application communicates through a backend-for-frontend layer, certificate-based authentication may be appropriate between trusted backend services depending on the threat model.

Backpressure

Migration can unintentionally increase request concurrency. Rapid navigation, reactive effects, polling, and duplicated subscriptions can produce bursts that the AngularJS application did not previously generate.

Token Bucket algorithms allow controlled bursts while Leaky Bucket approaches smooth traffic. In production, enforce rate limits at the backend or gateway because frontend throttling is not a security boundary.

interface TokenBucket { capacity: number; tokens: number; refillPerSecond: number; lastRefillMs: number; } const bucket: TokenBucket = { capacity: 20, tokens: 20, refillPerSecond: 5, lastRefillMs: Date.now() }; function allowRequest( nowMs: number ): boolean { const elapsedSeconds = (nowMs - bucket.lastRefillMs) / 1000; bucket.tokens = Math.min( bucket.capacity, bucket.tokens + elapsedSeconds * bucket.refillPerSecond ); bucket.lastRefillMs = nowMs; if (bucket.tokens < 1) { return false; } bucket.tokens -= 1; return true; }

This code demonstrates the token-bucket mechanism. A production implementation should normally live in an API gateway or backend service so the limit can be coordinated across application instances.

12. Production Best Practices

  • Freeze new AngularJS feature development.
  • Maintain a dependency inventory.
  • Use a supported Angular and Node.js combination.
  • Keep the lockfile committed.
  • Use deterministic CI installation.
  • Define feature-level migration boundaries.
  • Keep hybrid compatibility temporary.
  • Prefer standalone architecture for new migrated areas where appropriate.
  • Use signals for suitable local reactive state.
  • Use RxJS for stream-oriented asynchronous operations.
  • Make subscription lifetimes explicit.
  • Clean up global DOM listeners.
  • Lazy-load meaningful business domains.
  • Measure bundle and runtime performance.
  • Test authentication in production-like environments.
  • Maintain rollback capability during migration.
  • Delete migration-only compatibility code when the final dependency disappears.

13. Advanced Architectural Trade-Offs

Should the Entire Application Be Rewritten?

A rewrite gives the team freedom to design the target architecture from scratch, but it also creates a long period where the new implementation must reproduce existing business behavior. Incremental migration reduces the size of individual changes but temporarily introduces two architectures.

Evaluate application size, release frequency, test coverage, business tolerance for migration risk, and the ability to separate domains before choosing.

Should Everything Move to Signals?

No. Signals are useful for local reactive state and derived values. They do not automatically solve distributed state, server synchronization, event streams, complex asynchronous workflows, or cross-feature ownership.

Should RxJS Be Removed?

No. RxJS remains useful for HTTP workflows, WebSockets, event streams, retries, cancellation, debouncing, composition, and other asynchronous operations.

Should All NgModules Be Removed Immediately?

Not necessarily. Existing module-based Angular code can coexist with standalone components. The migration should reduce architectural complexity rather than forcing unrelated rewrites.

Should Backend APIs Be Redesigned at the Same Time?

Avoid unrelated backend redesign during the frontend framework migration unless the existing API blocks the migration. Changing the frontend architecture and backend contract simultaneously increases the number of possible failure sources.

How Much Compatibility Code Is Acceptable?

Enough to migrate a bounded feature safely, but not enough to recreate AngularJS architecture inside Angular. Every compatibility layer should have an owner and a removal condition.

When Should AngularJS Be Removed?

Remove it only after routing, templates, services, authentication integration, tests, analytics, and production workflows no longer depend on it. The final removal should be a deliberate architectural milestone.

14. Real-World Technical FAQ

1. Can AngularJS and modern Angular run in the same application?

Yes. Angular provides upgrade tooling that can support a hybrid application. The hybrid architecture should have clearly defined ownership boundaries and a plan for eventually removing the legacy runtime.

2. Should controllers be migrated one at a time?

Usually, migrate complete business capabilities instead. A route, component, service, state model, API integration, authorization rules, and tests form a stronger migration unit than an isolated controller.

3. Should every AngularJS service become an Angular injectable?

No. First understand what the service actually does. Some legacy services are better represented as pure functions, components, API clients, signals, route data, or dedicated state services.

4. Are signals a replacement for RxJS?

No. Signals are useful for reactive application state while RxJS remains valuable for asynchronous streams and event composition.

5. How can memory leaks be found after migration?

Repeat the same navigation workflow, capture heap snapshots, compare retained objects, inspect global event listeners, and verify that destroyed component trees become collectible.

6. Should standalone components be used during migration?

They are a reasonable architecture for newly migrated areas. Existing NgModule-based code does not need to be converted immediately unless doing so provides a concrete architectural benefit.

7. What is the biggest migration mistake?

Treating migration as syntax conversion. Hidden shared state, global events, lifecycle assumptions, duplicated HTTP requests, third-party dependencies, authentication behavior, and unclear ownership usually create more risk than TypeScript conversion itself.

15. Recommended Enterprise Migration Sequence

  1. Inventory the AngularJS application.
  2. Capture production performance and error baselines.
  3. Identify business domains.
  4. Freeze new AngularJS functionality.
  5. Choose a supported Angular toolchain.
  6. Build the modern application shell.
  7. Define authentication and HTTP ownership.
  8. Select the first bounded business capability.
  9. Introduce compatibility infrastructure only where required.
  10. Migrate service contracts.
  11. Migrate components and templates.
  12. Migrate state and lifecycle management.
  13. Move routing ownership.
  14. Measure performance and memory.
  15. Release through controlled rollout.
  16. Monitor production behavior.
  17. Repeat the process for the next capability.
  18. Remove the final AngularJS dependencies.
  19. Delete migration-only code.

16. Final Engineering Perspective

An AngularJS migration is complete when the organization has a simpler and better-defined architecture, not merely when the source files contain TypeScript.

The critical questions are architectural: who owns state, who owns routing, where asynchronous operations begin and end, how authentication is handled, which dependencies are actually required, and where compatibility boundaries exist.

Hybrid migration can be useful for large applications, but it has a real complexity cost. Standalone components, signals, RxJS lifecycle management, lazy loading, modern HTTP configuration, and current security practices should be introduced because they solve concrete engineering problems.

Measure every migration phase. Capture bundle size, API traffic, runtime performance, memory behavior, error rates, and user-facing metrics before and after major changes. Those measurements provide stronger evidence than generic claims that one framework is universally faster.

The final milestone is the removal of the AngularJS runtime and the compatibility infrastructure that supported it. If the application can remove those pieces without changing business behavior, the migration has reached its real architectural endpoint.

Comments