The build is 21. The source is not.
package.json says Angular 21.2. The CLI uses the application builder. The app still boots with platformBrowserDynamic().bootstrapModule(AppModule), the same shape it had on Angular 12.
Scoreboard
| Practice | This repo | Angular 21 |
|---|---|---|
| Bootstrap | bootstrapModule(AppModule) |
bootstrapApplication + ApplicationConfig |
| Components | 533 standalone: false, 17 explicit true |
Standalone by default |
| NgModules | 73 files | Optional, usually gone |
| Templates | 6,148 *ngIf, 763 *ngFor, 249 *ngSwitch |
@if / @for / @switch |
| New control flow | 0 | Default since 17 |
| Signals | 0 signal(), 0 computed(), 0 effect() |
Primary state model |
| Inputs / outputs / queries | 1,412 @Input, 400 @Output, 597 @ViewChild |
input(), output(), viewChild() |
| HTTP | HttpClientModule + class interceptors |
provideHttpClient(withFetch(), withInterceptors()) |
| Router | RouterModule.forRoot, class guards |
provideRouter, CanActivateFn, withComponentInputBinding() |
| SSR hydration | AppServerModule + CommonEngine, no hydration |
provideClientHydration, withEventReplay |
| OnPush | 92 of 666 components | OnPush + signals, then zoneless |
| Unsubscribe | 1,142 takeUntil(this.destroy$) |
takeUntilDestroyed() |
| Forms | 747 UntypedForm* |
Typed FormGroup / FormControl |
| Images | 0 NgOptimizedImage |
ngSrc |
| Deferral | 0 @defer |
Lazy heavy blocks in the template |
| Tests | Karma, zero specs | Vitest is the CLI default |
Counts from src/ on 22 Sep 2026. input(), output(), model(), toSignal(), afterNextRender(), and httpResource() do not appear.
What already moved
Keep The application builder in angular.json. That is the Angular 17+ build. A leftover @angular-devkit/build-angular:server target is still in the file. It only matters if serverless still uses it.
Keep provideZoneChangeDetection() in app.module.ts. Angular 21 stopped providing the zone scheduler by default. That line is a patch so NgModule bootstrap still works. It is not a modernization. It is how you stay on Zone while the rest of the source catches up.
Keep promotionalAuthGuard as a function that uses inject(). That is the 21 shape. AuthGuard, PermissionGuard, CreatorApprovalGuard, and the rest are still injectable classes.
Mixed A few newer files omit standalone: false, so Angular 19+ treats them as standalone. FormSelectComponent is one of those. It then imports HttpClientModule into the component. Half migrated, and the deprecated half.
Boot and DI
Blocker NgModule bootstrap
src/main.ts still calls enableProdMode(), then platformBrowserDynamic().bootstrapModule(AppModule), after a smoothscroll polyfill and a DOMContentLoaded wait. src/main.server.ts exports AppServerModule.
Angular 21 apps start with bootstrapApplication(AppComponent, appConfig) and provideServerRendering(). Production mode comes from the builder. enableProdMode() and the DOMContentLoaded gate go away with that move. Routing and providers need to be standalone first.
High HttpClientModule is deprecated
AppModule imports it. FormSelectComponent imports it again. Class interceptors register through HTTP_INTERCEPTORS.
The 21 setup is provideHttpClient(withFetch(), withInterceptors([authInterceptor, errorInterceptor])). withFetch() matters for SSR. XHR is the old browser path and fights hydration.
High Router is still a module
RouterModule.forRoot sets initialNavigation: 'enabledBlocking'. That flag is leftover. Blocking initial navigation is the default now.
Move to provideRouter(routes, withComponentInputBinding()). Route params become input() on the page component. ActivatedRoute.snapshot subscriptions can shrink after that.
High SSR has no hydration
AppServerModule imports AppModule and ServerModule. server.ts uses CommonEngine. There is no provideClientHydration() and no withEventReplay().
You prerender and SSR, then the browser rebuilds the tree. That is Angular 12 SSR. Hydration is the 21 reason to keep SSR.
Templates
*ngIf still in templates@if / @for / @switchprofile.component.html has 320 *ngIfs. message-container has 144. Those two files are why a mechanical control-flow migration is worth doing and why a human has to review it.
@for requires track. 163 trackBy functions exist. Most lists do not. Start on wallet and feed.
997 [ngClass] / [ngStyle] bindings. Angular 21 prefers [class.x] and [style.y], or a single class string. NgClass on a 300-branch template is how change detection stays expensive.
No @defer. Marketing layouts, video players, Stripe, Quill, and Highcharts should not be in the first paint. The lazy CSS split already exists. Defer the components next.
No NgOptimizedImage. A white-label site with banners and merch images should be using ngSrc, priority, and sizes.
State and change detection
Blocker Signals were never adopted
Zero signal(). Zero computed(). Zero effect(). Zero input() / output() / model() / viewChild(). Zero toSignal(). Zero httpResource().
Component state is fields plus BehaviorSubject plus NgRx Store with immutability checks off. 182 async pipes. 524 detectChanges() calls. 4 ngAfterViewChecked hooks. 231 NgZone mentions. 151 isPlatformBrowser checks.
Angular 21's path is signals, then OnPush, then zoneless. This app is still default CD and Zone, with a comment that Angular 21 made them opt back into Zone. Signals on profile, messages, home, and wallet come first.
High destroy$ instead of takeUntilDestroyed()
290 components implement OnDestroy. 1,142 takeUntil(this.destroy$) calls. takeUntilDestroyed() is unused.
Replace those with takeUntilDestroyed() or toSignal().
High Untyped reactive forms
747 UntypedFormGroup / UntypedFormControl / UntypedFormBuilder uses. That API exists so Angular 14 could ship typed forms without breaking old code. Three major versions later it is still the default here.
Convert each form as it is touched. The dynamic form builder stays generic.
Later NgRx classic
StoreModule.forRoot with strictStateImmutability: false. No effects. No @ngrx/signals. If the store stays a user-profile bag, a signal service covers it.
Modules, schemas, leftover platform
High SharedModule is 650 lines and imported by AppModule. Lazy routes then import it again. Angular 21 standalone components import what they use. Until that split happens, loadChildren is mostly theater.
High CUSTOM_ELEMENTS_SCHEMA is in 50 files, including AppModule. That hides missing imports. Standalone migration will fail in silence if this stays on the root module.
Cleanup styleUrls: ['...'] on 597 components. Angular 17 added styleUrl for the one-file case. Cosmetic. Change it when you touch the decorator.
Cleanup 153 @HostListener decorators. The 21 style is a host object on the decorator, or a host directive. Same for @HostBinding.
Cleanup polyfills.ts still talks about IE11 and Safari 10. It also sets window.global and a fake window.process so aws-sdk v2 can load. css-vars-ponyfill and smoothscroll-polyfill are the same era. Drop the IE story. The Node shims go when S3 uploads leave the browser.
Cleanup tsconfig still has module: es2020, emitDecoratorMetadata, downlevelIteration, newLine: crlf, strict: false, strictTemplates: false. Angular 21 templates can be checked. This repo turned that off. New standalone files should compile under strictTemplates even if the old ones cannot.
Cleanup Tests still target Karma. Angular 21's CLI generates Vitest. There are no specs, so switching costs nothing. Do it before the first real test file lands.
Cleanup @angular/pwa is still on v20. ngx-quill 16 and Quill 1.3.7 are mid-2010s editor stack. ServiceWorkerModule.register should become provideServiceWorker when bootstrap moves. BrowserAnimationsModule should become provideAnimationsAsync().
Order of work
- Replace
HttpClientModulewithprovideHttpClient(withFetch(), withInterceptors(...)). Convert the two interceptors to functions. - Add
provideClientHydration(withEventReplay()). - Replace
RouterModule.forRootwithprovideRouter(..., withComponentInputBinding()). Convert the class guards to functions. - Convert feature modules to standalone routes, starting with a leaf feature.
- Replace
*ngIf/*ngFor/*ngSwitchwith@if/@for/@switch. - Add
@deferaround video, Stripe, Quill, Highcharts, and emoji-mart. Switch images toNgOptimizedImage. - Replace
@Input/@Output/@ViewChildwithinput()/output()/viewChild(). - Replace
destroy$/takeUntilwithtakeUntilDestroyed(). ReplaceUntypedForm*with typed forms. - Add signals and OnPush on profile, messages, home, and wallet.
- Split
SharedModule. DropCUSTOM_ELEMENTS_SCHEMAfromAppModuleonce imports are explicit. - Replace
BrowserAnimationsModule,ServiceWorkerModule, andStoreModule.forRootwithprovideAnimationsAsync,provideServiceWorker, andprovideStore. - Replace
AppModulebootstrap withbootstrapApplication. DeleteAppServerModule. - Switch Karma to Vitest. Bump
@angular/pwato 21.