Anzeige
Anzeige
Anzeige
Anzeige
Lesedauer 6 Min.

Dependency Injection in Nest.js, Mehr als nur ein Container

Nest.js baut auf dem Dependency-Injection-Prinzip auf, setzt aber an einigen Stellen eigene Akzente.
© EMGenie

Dependency Injection gehört für .NET-Entwickler zum Alltag. AddScoped, AddSingleton, AddTransient, die Registrierung im DI-Container ist längst Routine. Nest.js basiert auf demselben Prinzip, weist aber einige Besonderheiten auf. In diesem Artikel sehen wir uns an, wie Nest.js Abhängigkeiten auflöst, welche Scopes es gibt, wie sich Custom Providers nutzen lassen und warum das Testing-Modul ein echtes Highlight ist.

Scopes: Singleton ist der Standard

In ASP.NET Core entscheidet man bei der Registrierung, ob ein Service Scoped, Singleton oder Transient ist. AddScoped erstellt eine Instanz pro HTTP-Request, AddSingleton eine für die gesamte Anwendungslebensdauer, AddTransient jedes Mal eine neue. In Nest.js gibt es dieselben drei Konzepte, aber mit einem entscheidenden Unterschied: Der Standard-Scope ist Singleton.

Das ist kein Zufall, sondern eine bewusste Designentscheidung. Node.js arbeitet single-threaded mit einem Event Loop, es gibt kein Thread-Pool-Modell wie in ASP.NET Core, bei dem Scoped Services an den jeweiligen Request-Thread gebunden sind. Ein Singleton in Nest.js ist deshalb nicht dasselbe Risiko wie in einer Multi-Threaded-Umgebung: Es gibt keine Race Conditions durch parallele Threads. Für zustandslose Services, und das sind die meisten, ist Singleton die performanteste und einfachste Wahl.

Braucht man dennoch einen Request-gebundenen Scope, etwa für Multi-Tenancy, bei der jeder Request seinen eigenen Datenbankkontext benötigt, lässt sich das explizit konfigurieren:

 

@Injectable({ scope: Scope.REQUEST })
 export class TenantService {
   constructor(@Inject(REQUEST) private req: Request) {}
   getTenantId(): string {
     return this.req.headers[‘x-tenant-id’];
   }
 }

 

Das Pendant zu AddScoped ist also Scope.REQUEST, und Scope.TRANSIENT entspricht AddTransient. Wichtig zu wissen: Sobald ein Service einen Request-Scope hat, propagiert Nest.js diesen Scope durch die gesamte Abhängigkeitskette nach oben. Ein Controller, der einen Request-Scoped Service injiziert, wird selbst Request-Scoped. In .NET ist das nicht anders, dort warnt der Container sogar vor Captive Dependencies, wenn ein Scoped Service in einen Singleton injiziert wird. Nest.js löst das Problem elegant, indem es die Scope-Propagation automatisch handhabt.

Custom Providers: Flexibilität jenseits der Klasse

In ASP.NET Core kennt man Factory-Registrierungen: builder.Services.AddScoped(sp => new MyService(sp.GetService<IDep>())). Nest.js bietet vier Provider-Varianten, die noch einen Schritt weiter gehen: useClass für alternative Implementierungen, useValue für statische Werte oder Konfigurationsobjekte, useFactory für dynamische Erzeugung mit Abhängigkeiten und useExisting für Aliase auf bestehende Provider.

Ein praktisches Beispiel ist ein Datenbankprovider, der je nach Umgebung eine andere Verbindung aufbaut:

 

 

const dbProvider = {
   provide: ‘DATABASE_CONNECTION’,
   useFactory: async (config: ConfigService) => {
     const host = config.get(‘DB_HOST’);
     return createConnection({ host });
   },
   inject: [ConfigService],
 };

 

Das inject-Array gibt an, welche Abhängigkeiten die Factory erhält, ähnlich wie der ServiceProvider-Parameter in der .NET-Factory. Der Vorteil: Die Factory kann asynchron sein, was besonders bei Datenbankverbindungen oder externen Service-Initialisierungen nützlich ist. In .NET müsste man dafür auf Lazy<T> oder eine eigene Initialisierungslogik zurückgreifen.

Besonders useValue verdient Beachtung. Damit lassen sich Konfigurationsobjekte, Feature-Flags oder externe API-Clients als Provider registrieren – Werte, die keine eigene Klasse brauchen, aber trotzdem per DI verfügbar sein sollen. Das Pendant in .NET wäre IOptions<T> oder eine manuelle Registrierung mit builder.Services.AddSingleton(myConfig).

Injection Tokens: Wenn die Klasse nicht reicht

In der .NET-Welt registriert man Services gegen Interfaces: builder.Services.AddScoped<IUserService, UserService>(). Das Interface ist das Token, gegen das aufgelöst wird. In Nest.js ist die Klasse selbst das Token; das funktioniert, solange man genau eine Implementierung hat. Aber was, wenn man mehrere Logger-Implementierungen benötigt, etwa einen Logger für die Konsole und einen für ein externes System?

Hier kommen String-Tokens und der @Inject()-Decorator ins Spiel. Anstelle einer Klasse dient ein String als Schlüssel für die Auflösung:

 

 

// Registrierung im Modul
 providers: [
   { provide: 'CONSOLE_LOGGER', useClass: ConsoleLogger },
   { provide: 'EXTERNAL_LOGGER', useClass: ElkLogger },
 ]
 // Injection im Service
 constructor(
   @Inject('EXTERNAL_LOGGER') private logger: LoggerService,
 ) {}

 

Das Konzept ist vergleichbar mit Keyed Services, die Microsoft mit .NET 8 eingeführt hat. Dort registriert man mit builder.Services.AddKeyedScoped<ILogger, ElkLogger>("external") und löst mit [FromKeyedServices("external")] auf. Der Mechanismus ist derselbe, die Syntax unterscheidet sich. Wer zusätzliche Typsicherheit möchte, kann in Nest.js statt Strings auch Symbol-basierte Tokens oder die InjectionToken-Klasse verwenden; das verhindert versehentliche Namenskollisionen in größeren Projekten.

In der Praxis zeigt sich der Wert von Injection Tokens vor allem in modularen Architekturen. Wenn ein SharedModule einen Logger exportiert, aber das konsumierte Modul entscheiden soll, welche Implementierung es erhält, ermöglichen Tokens genau diese Entkopplung. Das Modul definiert den Token, der Consumer liefert die Implementierung – ein Muster, das in .NET mit Interface-Registrierung im Composition Root vertraut ist, in Nest.js aber über die Modul-Hierarchie gelöst wird.

Das Testing-Modul: DI als Testbarkeits-Turbo

Der eigentliche Payoff von Dependency Injection zeigt sich beim Testen. In .NET erstellt man typischerweise Mocks mit Moq oder NSubstitute und übergibt sie manuell an den Konstruktor. Das funktioniert, erfordert aber für jeden Test das manuelle Verdrahten aller Abhängigkeiten. Nest.js bietet hier einen eleganteren Weg: das Testing-Modul.

Mit Test.createTestingModule() baut man einen eigenen DI-Container für den Test auf, mit allen echten Providern des Moduls, aber der Möglichkeit, einzelne davon gezielt zu überschreiben:

 

const module = await Test.createTestingModule({
   providers: [UsersService, UsersRepository],
 })
 .overrideProvider(UsersRepository)
 .useValue({ findAll: jest.fn().mockResolvedValue([]) })
 .compile();
 const service = module.get(UsersService);

 

Das Testing-Modul baut den vollständigen DI-Graphen auf, ersetzt aber gezielt das UsersRepository durch einen Mock. Der UsersService erhält den Mock über den normalen DI-Mechanismus; er weiß nicht, dass er in einem Test läuft. Das Konzept ist vergleichbar mit WebApplicationFactory<T> in ASP.NET Core, nur granularer: Man muss nicht die gesamte Anwendung hochfahren, sondern kann einzelne Module isoliert testen.

Besonders hilfreich ist das Fluent-API mit overrideProvider(). In .NET müsste man dafür entweder den Service manuell im Konstruktor verdrahten oder mit ConfigureTestServices in der WebApplicationFactory arbeiten. In Nest.js ist das Override direkt im Test-Setup integriert: Weniger Boilerplate, mehr Fokus auf die eigentliche Testlogik.

Dasselbe Prinzip, andere Akzente

Dependency Injection in Nest.js ist kein Fremdkonzept. Es ist das gleiche Prinzip, das .NET-Entwickler seit Jahren nutzen, mit ein paar klugen Variationen. Singleton als Default statt Scoped, klassenbasierte Tokens statt Interface-Registrierung, asynchrone Factories als Standardfeature und ein Testing-Modul, das DI-basiertes Testen zum Kinderspiel macht. Wer die DI-Konzepte aus .NET verinnerlicht hat, wird sich in Nest.js nicht umgewöhnen müssen. Es gilt nur umzudenken an den Stellen, an denen Node.js andere Spielregeln vorgibt.

Im nächsten und letzten Teil dieser Serie werden wir den Schritt von der Theorie in die Praxis unternehmen: Wie sieht die Request-Pipeline mit Guards und Interceptors konkret aus? Wie bindet man eine Datenbank an? Und wie testet man eine Nest.js-Anwendung effizient im Alltag? Der Abschluss der Serie zeigt, wie das Gelernte in produktionsreifen Code übersetzt wird.

 

 

Neueste Beiträge

SQLite in ein .NET-Projekt integrieren - SQLite für .NET-Entwickler, Teil 2
Der eleganteste Aspekt von SQLite in .NET ist die Migration vom Prototyp zur Produktion.
6 Minuten
libSQL und Turso: SQLite für verteilte Systeme - SQLite für .NET-Entwickler, Teil 3
libSQL und Turso lösen die größte Einschränkung von SQLite: die Bindung an eine einzelne Instanz.
6 Minuten
15. Apr 2026
Wissen trifft Community - DWX 2026
Wohlfühlzeit mit intensivem Lernen für Entwickler:innen auf der DWX in Mannheim.
9 Minuten
16. Apr 2026

Das könnte Dich auch interessieren

SignalRC baut auf DRY - Der DDC-Truck, Teil 8
DRY ist eines dieser Prinzipien, die jeder für selbstverständlich hält, die aber trotzdem oft nicht konsequent umgesetzt werden. In SignalRC ist das Shared-Projekt von Beginn an dabei.
11 Minuten
12. Mär 2026
SignalRC mit ReactFlow – alles im Fluss - Der DDC-Truck, Teil 9
ReactFlow verwandelt ein abstraktes Signalverarbeitungsproblem in etwas, das man buchstäblich sehen und anfassen kann. Dabei ist die Signalverarbeitungskette vollständig datengetrieben.
13 Minuten
19. Mär 2026
SignalRC und Ping - Der DDC-Truck, Teil 10
Wie schnell ist die Verbindung zwischen Browser und Fahrzeug eigentlich?
9 Minuten
26. Mär 2026
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige