Mert Özen Her Satırda Daha İleri
Frontend Angular — #22

Angular 15 ile Standalone API'ler: bootstrapApplication, provideRouter ve NgModule'süz Uygulama

Mert Özen 31 Eki 2026 7 dk 0 görüntülenme
Angular 15 ile Standalone API'ler: bootstrapApplication, provideRouter ve NgModule'süz Uygulama

Angular 15 ile standalone API'ler stable oldu. bootstrapApplication, provideRouter, provideHttpClient ve importProvidersFrom ile AppModule'süz uygulama kurup mevcut projeyi adım adım taşıyoruz.

Sürüm notu: Bu yazı Angular 15 ile gelen yenilikleri anlatır; kodlar Angular 15 içindir.

Standalone Artık Deneme Değil

Angular 14'te standalone component'lere ilk kez göz atmıştık; o zaman developer preview durumundaydılar. 16 Kasım 2022'de çıkan Angular 15 ile standalone API'ler stable oldu. Yani artık "deneyebilirsin" değil, "production'da güvenle kullanabilirsin" diyoruz.

Angular 15 o kadar çok şey getirdi ki onu iki yazıya böldük. Bu yazıda işin omurgasına bakacağız: standalone component, directive ve pipe'lar, uygulamayı AppModule olmadan başlatan bootstrapApplication, router ve HttpClient'ı module'süz ayağa kaldıran provideRouter ile provideHttpClient. Sonunda da mevcut bir NgModule projesini standalone'a taşımanın mantığını konuşacağız.

14'ten 15'e Geçiş

git commit -am "chore: state before Angular 15 update"
ng update @angular/core@15 @angular/cli@15

Angular 15'in resmi Node aralığı 14.20 ve üstü, 16.13 ve üstü ya da 18.10 ve üstü. Node 16 ile devam ediyorsan sürümünün en az 16.13 olduğundan emin ol; seride kurduğumuz Node 16'nın son sürümü bunu zaten karşılıyor. TypeScript tarafında en az 4.8 gerekiyor.

Migration'lar bu kez daha çok yapılandırma dosyalarıyla uğraşıyor. tsconfig.json'da derleme hedefini ES2022'ye çekip useDefineForClassFields ayarını false yapıyor. CLI'nin varsayılanıyla aynı içeriğe sahip .browserslistrc dosyasını siliyor. Router'da artık geçerli olmayan relativeLinkResolution ayarını kaldırıyor ve RouterLinkWithHref kullanımlarını RouterLink'e çeviriyor.

Bir de CLI tarafında dikkat çeken bir sadeleşme var. Angular 15 ile ng new artık environments/ klasörünü, test.ts, polyfills.ts, karma.conf.js ve .browserslistrc dosyalarını üretmiyor. Bu yalnızca yeni projeler için geçerli; senin projendeki dosyalar yerinde kalıyor. Bir detayı da baştan söyleyelim: standalone API'ler stable olsa da Angular 15'te ng new hâlâ AppModule'lü bir proje oluşturuyor.

Başlangıç Noktası: Angular 12'de AppModule

Daha önce gördüğümüz klasik yapıyı hatırlayalım. Bir ürün listesi, bir directive ve bir pipe'tan oluşan küçük bir uygulama:

@NgModule({
  declarations: [AppComponent, ProductListComponent, HighlightDirective, PriceTagPipe],
  imports: [BrowserModule, HttpClientModule, AppRoutingModule],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
platformBrowserDynamic().bootstrapModule(AppModule)
  .catch(err => console.error(err));

Burada AppModule bir apartman yönetimi gibi çalışıyor. Hangi component'in hangi directive'i kullanabileceğine, uygulamanın hangi service'lere erişeceğine o karar veriyor. main.ts de uygulamayı doğrudan bir component'ten değil, bu module'den başlatıyor. Standalone dünyasında bu sorumlulukları tek tek başka yerlere dağıtacağız.

bootstrapApplication: Uygulamayı Bir Component'ten Başlatmak

Angular 15'te aynı uygulamanın main.ts dosyası şöyle:

import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes),
    provideHttpClient()
  ]
}).catch(err => console.error(err));

bootstrapApplication uygulamayı doğrudan kök component'ten başlatıyor. Eskiden AppModule'ün imports ve providers dizilerinde duran uygulama geneli ayarlar artık ikinci parametredeki providers listesinde. BrowserModule'ü eklemene gerek yok; bootstrapApplication onu kendisi hallediyor.

Kök component de standalone olmak zorunda:

import { Component } from '@angular/core';
import { RouterLink, RouterOutlet } from '@angular/router';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet, RouterLink],
  template: `
    <nav>
      <a routerLink="/products">Ürünler</a>
    </nav>
    <router-outlet></router-outlet>
  `
})
export class AppComponent { }

Template'te router-outlet ve routerLink kullandığımız için bu ikisini imports'a ekledik. Angular 12'de bunları AppRoutingModule'ün export ettiği RouterModule üzerinden dolaylı olarak alıyorduk. Artık her component kendi alışveriş listesini kendisi yazıyor; neye ihtiyacı varsa onu açıkça alıyor.

Standalone Directive ve Pipe

Sadece component'ler değil, directive ve pipe'lar da standalone olabiliyor. Daha önce gördüğümüz attribute directive örneğinin standalone hâli:

import { Directive, ElementRef, HostListener, inject } from '@angular/core';

@Directive({
  selector: '[appHighlight]',
  standalone: true
})
export class HighlightDirective {
  private readonly el: ElementRef<HTMLElement> = inject(ElementRef);

  @HostListener('mouseenter') onEnter(): void {
    this.el.nativeElement.style.backgroundColor = '#fff3b0';
  }

  @HostListener('mouseleave') onLeave(): void {
    this.el.nativeElement.style.backgroundColor = '';
  }
}

Pipe için de aynı tek satır yeterli:

@Pipe({ name: 'priceTag', standalone: true })
export class PriceTagPipe implements PipeTransform {
  transform(value: number): string {
    return `${value.toLocaleString('tr-TR')} TL`;
  }
}

Bunları kullanan component, ikisini de kendi imports dizisine ekliyor:

@Component({
  selector: 'app-product-list',
  standalone: true,
  imports: [NgFor, HighlightDirective, PriceTagPipe],
  template: `
    <div *ngFor="let p of products" appHighlight>
      {{ p.name }} - {{ p.price | priceTag }}
    </div>
  `
})
export class ProductListComponent {
  products = [
    { name: 'Mekanik Klavye', price: 1450 },
    { name: 'Kablosuz Mouse', price: 620 }
  ];
}

Dikkat edersen CommonModule'ün tamamı yerine sadece NgFor'u aldık. Angular 14.1'den beri NgIf, NgFor, AsyncPipe gibi parçalar tek tek import edilebiliyor. CommonModule almak da hâlâ geçerli; tercih senin.

provideRouter ve Lazy Loading

Angular 12'de route'ları RouterModule.forRoot(routes) ile bir module'e bağlıyorduk. Standalone uygulamada route'lar düz bir dizi olarak kendi dosyasında duruyor:

import { Routes } from '@angular/router';
import { ProductListComponent } from './product-list/product-list.component';

export const routes: Routes = [
  { path: '', redirectTo: 'products', pathMatch: 'full' },
  { path: 'products', component: ProductListComponent },
  {
    path: 'products/:id',
    loadComponent: () => import('./product-detail/product-detail.component')
      .then(m => m.ProductDetailComponent)
  },
  {
    path: 'admin',
    loadChildren: () => import('./admin/admin.routes').then(m => m.ADMIN_ROUTES)
  }
];

Lazy loading tarafındaki fark önemli. Daha önce gördüğümüz loadChildren bir NgModule yüklüyordu: import('./admin/admin.module').then(m => m.AdminModule). Artık iki yeni seçenek var. loadComponent tek bir standalone component'i, loadChildren ise başka bir dosyada duran düz bir route dizisini yükleyebiliyor. Arada module yok. Bu iki özellik aslında Angular 14'te gelmişti; Angular 15 ile birlikte üzerine güvenle uygulama kurabileceğin bir zemin oldu.

Router ayarları da provideRouter'a fonksiyonlarla ekleniyor. Angular 12'deki RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules }) satırının karşılığı şu:

provideRouter(routes, withPreloading(PreloadAllModules))

withDebugTracing(), withInMemoryScrolling() ve withRouterConfig() gibi diğer özellikler de aynı şekilde eklenip çıkarılıyor. Kullanmadığın özellik pakete hiç girmediği için bu yapı bundle boyutuna da yardımcı oluyor.

provideHttpClient ve importProvidersFrom

HttpClient için Angular 12'de HttpClientModule'ü AppModule'e import ediyorduk. Angular 14'te standalone bir uygulama kurmak isteyenler, module tabanlı bir paketi providers listesine taşımak için importProvidersFrom kullanıyordu:

bootstrapApplication(AppComponent, {
  providers: [importProvidersFrom(HttpClientModule)]
});

Angular 15 bunun için doğrudan bir fonksiyon getirdi: yukarıdaki main.ts'te gördüğün provideHttpClient(). Daha kısa, daha net ve ilerleyen yazıda göreceğimiz yeni özelliklere açık.

importProvidersFrom yine de çöpe gitmiyor. Henüz provide... fonksiyonu sunmayan bir kütüphane, ya da kendi yazdığın ve forRoot() ile ayar alan bir NgModule için köprü görevi görüyor. Eski bir fişi yeni prize takmanı sağlayan adaptör gibi düşün.

Daha önce gördüğümüz class-based interceptor'ların varsa, onları da kaybetmiyorsun:

providers: [
  provideHttpClient(withInterceptorsFromDi()),
  { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
]

withInterceptorsFromDi(), HTTP_INTERCEPTORS token'ı ile kaydedilmiş interceptor'ların eskisi gibi çalışmasını sağlıyor. Interceptor'ların fonksiyon olarak yazılan yeni hâline bir sonraki yazıda bakacağız.

Mevcut Projeyi Taşımanın Mantığı

Elinde yüzlerce component'li bir NgModule projesi varsa, hepsini bir hafta sonunda standalone'a çevirmek zorunda değilsin. İki dünya yan yana yaşayabiliyor; Angular 14 yazısında standalone bir component'i NgModule'ün imports dizisine eklediğimizi hatırla.

Bu yüzden en sağlıklı yol yapraklardan köke doğru ilerlemek. Önce başka hiçbir şeye bağımlı olmayan küçük parçalardan başla: pipe'lar, directive'ler, buton ya da kart gibi basit component'ler. Her birini standalone: true yap, ihtiyaç duyduğu şeyleri kendi imports'una ekle ve module'de declarations'tan imports'a taşı. Sonra feature module'lere geç; lazy yüklenen bir module'ü bir route dizisine çevirip loadChildren ile yükle. AppModule en son gider; her şey hazır olduğunda main.ts'i bootstrapApplication'a çevirirsin.

Bu işi elle yapmak zorunda da değilsin. Angular 15.2 ile bir migration schematic'i geldi:

ng generate @angular/core:standalone

Komut sana üç mod sunuyor ve bunları sırayla çalıştırman öneriliyor. İlki bütün component, directive ve pipe'ları standalone'a çeviriyor. İkincisi artık gereksiz kalan NgModule class'larını siliyor. Üçüncüsü uygulamayı bootstrapApplication ile başlatacak şekilde main.ts'i değiştiriyor. Her moddan sonra build ve testleri çalıştırıp commit atmak, bir şey ters gittiğinde neyin bozduğunu bulmayı kolaylaştırır.

Küçük Bir Deneme

Yeni bir Angular 15 projesi aç ya da elindeki projeyi 15'e yükselt. Önce yukarıdaki PriceTagPipe'ı standalone olarak yaz ve AppModule'ün declarations'ı yerine imports'una ekle. Sonra ng g c product-list --standalone ile bir component üret, pipe'ı onun imports'una ekle ve ekranda gör.

Biraz daha cesaretliysen bir adım ileri git: AppComponent'i standalone yap, app.routes.ts dosyasında route'larını tanımla ve main.ts'i bootstrapApplication ile yeniden yaz. AppModule'ü sildiğinde uygulama hâlâ açılıyorsa taşıma tamamdır. Bir kez de provideHttpClient() satırını silip bir service'te HttpClient kullanmayı dene; aldığın "No provider" hatası, provider'ların artık nerede durduğunu sana çok net gösterecek.

Standalone omurga hazır. Angular 15'in ikinci yarısında bu omurganın üzerine kurulan yeniliklere bakacağız: fonksiyon olarak yazılan guard'lar ve interceptor'lar, directive'leri birleştirmeyi sağlayan hostDirectives ve görselleri optimize eden NgOptimizedImage.