Analyze with AI
Get AI-powered insights from this Mad Devs article:
An AngularJS application rarely stops changing while its replacement is being built. Customers still need fixes, product teams still request features, and integrations still evolve. That makes an AngularJS to Angular migration a delivery problem as much as a framework problem. A big-bang replacement concentrates too much unverified behavior into one release, while an open-ended hybrid can become another legacy system.
This guide explains how to define a bounded first stage, select a coexistence model, preserve observable behavior, and set an explicit condition for removing every temporary bridge. If the broader choice between refactor or rebuild is still unresolved, settle that decision first. Here, Angular is already the target.
Key takeaways
- Migration difficulty follows dependency boundaries, not the number of controllers or components.
- A hybrid architecture is an option to validate, not a default. Its runtime, routing, and change-detection assumptions need explicit testing.
- The first increment should complete a bounded user flow and test the proposed architecture under real navigation and data conditions.
- Progress includes removing AngularJS routes, services, adapters, and build paths. Counting new Angular components is not enough.
Set the migration target and constraints
AngularJS 1.x and Angular are different frameworks. This is not a package upgrade, and ng update cannot translate an AngularJS application into Angular. Angular's release documentation explicitly distinguishes AngularJS as the name for version 1.x from Angular 2 and later. The official AngularJS documentation marks support as ended as of January 2022, but support status alone does not define the business priority or the safest migration route.
Choose a currently supported Angular release by policy rather than putting a version number into a long-lived plan. Then verify the exact compatibility range for Node.js, TypeScript, and RxJS in Angular's version compatibility table. Audit the browser matrix, build agents, package registry, testing tools, and critical UI libraries against that target. An apparently small library incompatibility can determine the boundary of the first viable increment.
Record constraints in terms the team can test:
- user flows, validation rules, permissions, and deep links that must keep working;
- API contracts and session behavior that the frontend must preserve;
- supported browsers and accessibility requirements;
- release windows, rollback options, monitoring, and feature work that will continue during migration;
- dependencies that cannot run in the target build or have no maintained replacement.
The result should be a target stack and a short constraint register, not a promise to rewrite everything. If a required dependency cannot operate with the chosen Angular version, revisit the dependency, the migration boundary, or the target decision before estimating delivery.
Map the dependencies that determine migration order
A file count says little about migration order. A small directive that reads $rootScope, manipulates the DOM, and invokes a jQuery plugin can be harder to isolate than a large component with a clean API. Build a dependency map around behavior and ownership.
Start with routes and user flows, then trace inward. For every candidate area, identify controllers and component directives, shared services, global events, state, route guards, and backend calls. Search for direct DOM manipulation, $compile, transclusion assumptions, and jQuery plugins. Check whether third-party UI controls have Angular-compatible replacements and whether their behavior is covered by tests. Include build and deployment dependencies because a flow is not independently releasable if it requires an untested second pipeline.
| Area | Dependency or blocker | Migration implication |
|---|---|---|
| Record detail route | Uses an AngularJS route resolver and permission guard | Assign one router as navigation authority and preserve guard behavior before switching the route |
| Record editor | Reads shared state from $rootScope events | Replace the implicit event contract or expose a narrow bridge with a removal owner |
| API client | AngularJS service is used by several legacy screens | Upgrade the service temporarily, wrap the API behind a framework-neutral contract, or change the pilot boundary |
| Date picker | jQuery plugin mutates child DOM | Replace it or isolate DOM ownership so Angular does not reconcile the same nodes |
| Build pipeline | Legacy templates require a separate loader | Prove both production builds and asset paths in the pilot environment |
| Critical save flow | Only unit tests exist | Add characterization and end-to-end coverage before changing implementation |
Use the map to find seams: a route subtree, a feature module, or a workflow whose inputs and outputs can be stated precisely. Score candidates by coupling, business criticality, testability, and release independence. A low-risk leaf component can prove component bridging, but it may avoid the routing, state, and API questions that dominate the full migration. Conversely, choosing the most coupled checkout or administration flow first may combine too many unknowns.
The map is a decision artifact, not an exhaustive API catalog. Update it as the team discovers hidden event listeners or shared state. Do not require every controller to be rewritten before useful migration work begins.
Choose how AngularJS and Angular will coexist
There are two common transition shapes. A hybrid application runs both frameworks in one page and connects selected components and services. A route-boundary migration gives Angular complete ownership of a route or section and keeps integration at explicit navigation, authentication, and API boundaries. A small, well-tested application may instead be replaceable as one release, but that conclusion should follow dependency analysis.
The official UpgradeModule documentation describes helpers in @angular/upgrade/static for using AngularJS and Angular components together and exposing services across injectors. An AngularJS component can be upgraded for use in Angular; an Angular component can be downgraded for use in AngularJS. These wrappers enable interaction. They do not translate controllers, templates, or services into modern Angular source code.
For a hybrid, write down five ownership decisions before implementation:
- Application shell: which framework bootstraps the root and controls global layout.
- DOM: which framework owns each template and the child view created across a component boundary. The Angular documentation notes that the framework instantiating the host owns that host, while the other owns the component view.
- Navigation: which router is authoritative and how URLs, guards, redirects, and browser history cross the boundary.
- State and services: which dependencies are upgraded, downgraded, replaced, or made framework-neutral.
- Temporary code: who owns every wrapper and what event permits its removal.
Treat change detection as a compatibility test, not an assumption. UpgradeModule exposes the bootstrap NgZone, and its documented hybrid model has specific bootstrap and DOM-ownership semantics. Verify those semantics against the selected Angular configuration, third-party callbacks, and asynchronous AngularJS behavior. A standard new-application setup may not exercise the same path as a hybrid.
Route-level separation often reduces shared DOM and injector complexity. It works best when authentication, navigation, design tokens, and API access can cross a stable boundary. It can be harder when a single page contains deeply interleaved widgets or global AngularJS state. Choose the model that makes ownership explicit with the fewest temporary contracts, not the model with the shortest setup tutorial.
Migrate one bounded user flow
Consider a hypothetical records area with a list, a detail view, an editor, a shared API service, and deep links such as /records/42. The example is instructional, not a production implementation.
First, set the boundary. The increment is complete when a permitted user can open the list, follow or directly enter a detail URL, edit a record, save it, and return without losing navigation state. The dependency map shows that the API client is shared with legacy screens and that permission checks live in the AngularJS router.
Second, choose compatibility deliberately. Suppose the team selects route ownership: Angular owns /records/**, while AngularJS retains the rest of the shell during the pilot. Authentication is read through an existing stable session contract. The new records API client follows the current HTTP contract rather than importing an AngularJS controller. If the shared AngularJS service contains indispensable transformation logic, expose only that narrow behavior through a temporary adapter and add its deletion condition to the increment.
Third, implement the Angular list, detail, and editor as one flow. Keep API payloads stable unless backend change is explicitly in scope. Reproduce validation and error behavior from characterization tests, not from assumptions about what the old UI intended to do.
The responsibility sequence is:
- The authoritative router receives /records/42 and applies the existing session and permission rules.
- The Angular records area loads the API contract and renders the detail view.
- The editor validates and saves through the Angular client or the documented temporary adapter.
- Cross-boundary links use canonical URLs, so reload, back, and forward navigation do not depend on hidden in-memory state.
- The release switch directs the records route to Angular for the selected rollout group.
- After the legacy records consumers are gone, the team removes their route definitions, controllers, templates, and any records-specific adapter.
Starting with a leaf component can still be useful when the primary uncertainty is upgrade or downgrade behavior. However, rendering a new badge inside an old page proves neither route ownership nor a complete customer outcome. The first flow should be small enough to control and broad enough to test the architecture that later increments will reuse.
Do not impose a universal order such as “all services first, then all components.” In another codebase, replacing a global service first may expand risk across every screen. Let the dependency map determine whether to bridge, replace, or move the boundary.
Validate behavior before expanding the rollout
Acceptance should compare observable behavior, not implementation shape. Poorly documented legacy behavior needs characterization tests before replacement: capture what the application does for representative inputs, including awkward error paths, even when the team later decides that some behavior should change.
| Check | Expected result |
|---|---|
| Direct URL entry and refresh | The requested list or record opens with the correct session and permissions |
| Navigation between old and new areas | Canonical URLs, redirects, back, and forward behave as designed |
| Read, edit, and save | API payloads and required business behavior remain compatible |
| Forms and validation | Required, conditional, and server-side validation is preserved or intentionally changed |
| Session and authorization | Existing access restrictions and expiry behavior remain effective |
| API failures and browser navigation | Errors, retry paths, unsaved changes, back, and forward are handled correctly |
| Loading and interaction | Measured behavior stays within the acceptance threshold defined from the baseline |
Use test levels for different risks. Unit tests cover transformation, validation, and component logic. Integration tests verify the router, injectors or adapters, API client, and framework boundary. End-to-end tests protect the complete flow across login, navigation, save, refresh, and failure cases. A universal coverage percentage is less useful than evidence that the high-risk contracts in the dependency map are exercised.
Establish a baseline before switching traffic. That may include request failure rate, frontend exceptions, route success, interaction timing, and business completion signals already available in the product. Define acceptable change and the observation window for this application. Avoid claiming that framework replacement automatically improves performance: bundle duplication and hybrid change detection can create temporary regressions.
Use a limited rollout where the delivery system supports it, with named owners watching both technical and user-flow signals. Decide the response to each failure class in advance: disable the route switch, fix forward, or invoke a tested rollback. Returning to the old frontend is not automatically safe if a release also changed API behavior or persisted data. A rollback plan must cover those contracts, not only static assets.
Expand only after the pilot demonstrates that the coexistence boundary is operable, the flow meets acceptance criteria, and temporary dependencies have a credible removal path. Otherwise, revise the architecture while the affected surface is still small.
Retire AngularJS and close the transition
A hybrid is a temporary architecture with production responsibilities. Give it an owner, track its bridges, and budget deletion work in each increment. Without a retirement plan, wrappers become undocumented permanent interfaces and the team must understand two frameworks for every related change.
Use dependency evidence to remove legacy code. Search alone is insufficient because routes can be loaded dynamically and templates can reference directives by string. Combine static dependency analysis, route inventories, build output, runtime monitoring, and regression tests. For each completed area, verify that none of the following are still used:
- AngularJS routes, controllers, component directives, filters, and templates;
- legacy services and cross-injector dependencies exposed through adapters;
- AngularJS bootstrap code and upgrade or downgrade wrappers;
- old loaders, build targets, packages, polyfills, and test workarounds.
Remove a bridge when both sides of its contract no longer need it, then rerun the relevant behavior checks. Do not defer every deletion until the end. Incremental removal tests whether the architecture is actually converging and prevents new code from adopting a supposedly temporary service.
Track progress through retired behavior and reduced compatibility surface: routes transferred, legacy dependency edges removed, adapters deleted, and build paths consolidated. A rising Angular component count can coexist with an unchanged AngularJS core, so it is not a completion metric.
The transition is closed only when required user flows run on the target implementation; AngularJS and unnecessary compatibility code are gone; build, tests, monitoring, operational documentation, and dependency maintenance match the new architecture; and a team owns ongoing support. If the target Angular version or its dependencies moved during a long program, revalidate the supported stack before the final removal release.
A lesson from gradual frontend modernization
Mad Devs has described gradual TypeScript adoption in an existing JavaScript, jQuery, and Webpack project. The team adapted Webpack and linting, introduced TypeScript into new files, and continued converting existing code over time. The useful lesson is not that every frontend migration should copy those tools. It is that modernization has to fit the delivery system that continues to produce product work.
That principle changes an AngularJS migration plan. Build compatibility, testing, ownership, and deletion cannot be postponed as “cleanup after the rewrite.” They are part of each releasable increment. Feature work also needs a placement rule: teams should decide whether a changed area remains in AngularJS, moves into the current Angular boundary, or justifies adjusting the sequence. Otherwise, new dependencies can increase the legacy surface while the migration reports progress elsewhere.
The comparison has a firm limit. Adding TypeScript to a JavaScript codebase does not prove that two UI frameworks can share routing, DOM ownership, dependency injection, or change detection in a particular application. Those AngularJS-to-Angular questions still require a pilot and application-specific evidence.
A separate historical example confirms that organizations have commissioned this kind of work. In 2022, Ushahidi described working with ProCoders on a planned AngularJS-to-Angular 14 modernization. It supports the existence and stated scope of that engagement, not a recommendation to target Angular 14 today, a verified completion result, or a general delivery timeline.
Start with a pilot that can finish
Select one candidate flow and document four things: its dependency boundary, coexistence mechanism, behavioral acceptance tests, and the event that permits legacy deletion. If no meaningful flow can be bounded, investigate routing, global state, and service ownership before estimating the entire migration. That discovery is more valuable than a schedule built from component counts.
Plan your AngularJS migration
Define the migration scope, compatibility boundaries, and rollout with Mad Devs.
Discuss your project





