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

6,148*ngIf still in templates
0@if / @for / @switch

profile.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

  1. Replace HttpClientModule with provideHttpClient(withFetch(), withInterceptors(...)). Convert the two interceptors to functions.
  2. Add provideClientHydration(withEventReplay()).
  3. Replace RouterModule.forRoot with provideRouter(..., withComponentInputBinding()). Convert the class guards to functions.
  4. Convert feature modules to standalone routes, starting with a leaf feature.
  5. Replace *ngIf / *ngFor / *ngSwitch with @if / @for / @switch.
  6. Add @defer around video, Stripe, Quill, Highcharts, and emoji-mart. Switch images to NgOptimizedImage.
  7. Replace @Input / @Output / @ViewChild with input() / output() / viewChild().
  8. Replace destroy$ / takeUntil with takeUntilDestroyed(). Replace UntypedForm* with typed forms.
  9. Add signals and OnPush on profile, messages, home, and wallet.
  10. Split SharedModule. Drop CUSTOM_ELEMENTS_SCHEMA from AppModule once imports are explicit.
  11. Replace BrowserAnimationsModule, ServiceWorkerModule, and StoreModule.forRoot with provideAnimationsAsync, provideServiceWorker, and provideStore.
  12. Replace AppModule bootstrap with bootstrapApplication. Delete AppServerModule.
  13. Switch Karma to Vitest. Bump @angular/pwa to 21.