TypeScript mit NestJS: Grundlagen fuer typsichere Backend-Architektur
AI generated
<T>
type
TypeScript · NestJS · Node.js · Backend
TypeScript mit NestJS: Grundlagen
fuer eine typsichere Backend-Architektur

NestJS kombiniert TypeScript-Decorators, Dependency Injection und eine modulare Architektur zu einem Backend-Framework, das Struktur erzwingt statt sie dem Zufall zu ueberlassen. Wer TypeScript mit NestJS lernt, versteht Module, Controller, Provider, DTOs und Guards als zusammenhaengendes System, nicht als lose Sammlung von Funktionen.

18 Min. Lesezeit Module · Provider · DTOs · Guards · Testing NestJS 10.x · TypeScript 5.x

1. Warum NestJS TypeScript als Fundament nutzt

NestJS ist kein Framework, das TypeScript nachtraeglich unterstuetzt, sondern eines, das ohne TypeScript kaum denkbar waere. Wer TypeScript mit NestJS aufsetzt, merkt sofort, dass Decorators wie @Controller(), @Injectable() und @Module() auf Metadaten beruhen, die zur Compile-Zeit aus Typinformationen erzeugt werden. Ohne TypeScript wuerde die automatische Dependency Injection von NestJS erheblich mehr manuellen Aufwand erfordern, weil der Reflector die Konstruktor-Parametertypen nicht mehr ableiten koennte.

Der zweite Grund liegt in der Architektur selbst. NestJS uebernimmt bewusst Konzepte aus Angular, darunter Module als Bausteine, Provider als injizierbare Dienste und ein hierarchisches Dependency-Injection-System. Diese Konzepte funktionieren nur zuverlaessig, wenn der Compiler Typfehler bereits vor der Ausfuehrung meldet. Ein Controller, der einen falschen Service-Typ erwartet, faellt bei TypeScript mit NestJS sofort beim Kompilieren auf, statt erst zur Laufzeit einen kryptischen Fehler zu werfen.

Fuer Teams, die aus dem PHP- oder Java-Umfeld kommen, wirkt NestJS deshalb vertraut: Klassen, Interfaces, Decorators und eine klare Trennung von Zustaendigkeiten ersetzen die freie Funktionsorganisation, die man aus reinem Express-Code kennt. Genau diese Struktur macht NestJS in groesseren Teams beliebt, in denen Konsistenz wichtiger ist als maximale Flexibilitaet.

2. Module, Controller und Provider: Die Grundstruktur

Jede NestJS-Anwendung besteht aus mindestens einem Root-Modul, das mit @Module() dekoriert ist und Controller sowie Provider referenziert. Ein Modul kapselt einen fachlichen Bereich, etwa UsersModule oder OrdersModule, und exportiert nur die Provider, die andere Module tatsaechlich benoetigen. Diese explizite Export-Liste verhindert, dass interne Implementierungsdetails ungewollt ausserhalb des Moduls sichtbar werden.

Controller sind fuer das Routing zustaendig und delegieren die eigentliche Logik an Services. Ein typisches Muster bei TypeScript mit NestJS ist ein duenner Controller, der Parameter mit Decorators wie @Param(), @Body() und @Query() entgegennimmt und direkt an eine typisierte Service-Methode weiterreicht. Die Rueckgabetypen der Controller-Methoden werden von NestJS automatisch serialisiert, was Typfehler bei falschen Rueckgaben schon im Editor sichtbar macht.


// users.module.ts — feature module with explicit exports
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({
  controllers: [UsersController],
  providers: [UsersService],
  exports: [UsersService], // only export what other modules truly need
})
export class UsersModule {}

// users.controller.ts — thin controller, delegates to the service
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import type { User } from './user.entity';

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get(':id')
  async findOne(@Param('id') id: string): Promise<User> {
    return this.usersService.findOneOrFail(id);
  }

  @Post()
  async create(@Body() dto: CreateUserDto): Promise<User> {
    return this.usersService.create(dto);
  }
}

3. Dependency Injection mit Decorators verstehen

Das Herzstueck von TypeScript mit NestJS ist die Dependency Injection. Ein Provider, meist eine mit @Injectable() markierte Klasse, wird bei Bedarf automatisch instanziiert und in andere Klassen eingespeist, deren Konstruktor ihn per Typ anfordert. NestJS liest dazu die Design-Time-Typinformationen aus, die der TypeScript-Compiler mit der Option emitDecoratorMetadata in tsconfig.json erzeugt. Ohne diese Option funktioniert die automatische Injection nicht zuverlaessig.

Scopes steuern die Lebensdauer eines Providers. Der Standard-Scope DEFAULT erzeugt eine Singleton-Instanz fuer die gesamte Anwendung, waehrend REQUEST pro eingehendem Request eine neue Instanz anlegt, etwa fuer request-spezifische Kontextdaten wie den eingeloggten Benutzer. Custom Provider mit useFactory oder useValue erlauben es, auch Konfigurationswerte oder externe Clients als typisierte, injizierbare Abhaengigkeiten bereitzustellen, statt sie global zu importieren.

Ein haeufiger Fehler bei TypeScript mit NestJS: Interfaces koennen nicht direkt als Injection Token verwendet werden, weil TypeScript-Interfaces zur Laufzeit nicht existieren. Fuer diesen Fall braucht man ein InjectionToken, das explizit mit @Inject() referenziert wird, waehrend das Interface weiterhin fuer die Typpruefung zur Compile-Zeit genutzt wird.


// payment.tokens.ts — injection token for an interface-based provider
export const PAYMENT_GATEWAY = Symbol('PAYMENT_GATEWAY');

export interface PaymentGateway {
  charge(amountCents: number, currency: string): Promise<{ id: string }>;
}

// payment.module.ts — bind the interface to a concrete implementation
import { Module } from '@nestjs/common';
import { StripeGateway } from './stripe.gateway';
import { PAYMENT_GATEWAY } from './payment.tokens';

@Module({
  providers: [
    { provide: PAYMENT_GATEWAY, useClass: StripeGateway },
  ],
  exports: [PAYMENT_GATEWAY],
})
export class PaymentModule {}

// checkout.service.ts — inject the interface via its token
import { Inject, Injectable } from '@nestjs/common';
import { PAYMENT_GATEWAY, type PaymentGateway } from './payment.tokens';

@Injectable()
export class CheckoutService {
  constructor(
    @Inject(PAYMENT_GATEWAY) private readonly gateway: PaymentGateway,
  ) {}

  async pay(amountCents: number): Promise<string> {
    const result = await this.gateway.charge(amountCents, 'EUR');
    return result.id;
  }
}

4. DTOs und Validation Pipes fuer typsichere Eingaben

Data Transfer Objects sind bei TypeScript mit NestJS die Bruecke zwischen Laufzeitvalidierung und Compile-Zeit-Typisierung. Eine DTO-Klasse definiert mit Decorators aus class-validator, welche Regeln ein eingehendes JSON-Objekt erfuellen muss, waehrend die Klasse gleichzeitig als TypeScript-Typ fuer die weitere Verarbeitung dient. Dieser doppelte Nutzen ist deutlich robuster als separate Validierungslogik, die von der Typdefinition abweichen kann.

Die globale ValidationPipe wandelt eingehende Plain Objects mit class-transformer automatisch in Instanzen der DTO-Klasse um und prueft anschliessend jede Validierungsregel. Schlaegt eine Regel fehl, wirft NestJS automatisch eine BadRequestException mit einer strukturierten Fehlerliste, ohne dass der Controller manuell pruefen muss. Damit wird Validierung zu einer deklarativen, typsicheren Angelegenheit statt einer verstreuten Sammlung von if-Abfragen.


// create-user.dto.ts — validation rules double as the compile-time type
import { IsEmail, IsString, MinLength, IsOptional, IsEnum } from 'class-validator';

export enum UserRole {
  Customer = 'customer',
  Admin = 'admin',
}

export class CreateUserDto {
  @IsEmail()
  email!: string;

  @IsString()
  @MinLength(8)
  password!: string;

  @IsOptional()
  @IsEnum(UserRole)
  role?: UserRole;
}

// main.ts — enable global validation with whitelisting
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';

async function bootstrap(): Promise<void> {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true, // strips unknown properties instead of accepting them
      forbidNonWhitelisted: true,
      transform: true, // converts plain objects to typed DTO instances
    }),
  );
  await app.listen(3000);
}
bootstrap();

5. Guards: Authentifizierung und Autorisierung typsicher umsetzen

Guards entscheiden vor dem Aufruf eines Handlers, ob ein Request weiterverarbeitet werden darf. Anders als klassische Express-Middleware haben Guards Zugriff auf den vollen ExecutionContext und koennen so sowohl HTTP-Requests als auch WebSocket- oder RPC-Aufrufe einheitlich behandeln. Bei TypeScript mit NestJS implementiert ein Guard das CanActivate-Interface, dessen Rueckgabewert ein Boolean, ein Promise davon oder ein Observable sein kann.

Rollenbasierte Autorisierung kombiniert Guards typischerweise mit Custom Decorators und Reflection. Ein @Roles('admin')-Decorator setzt Metadaten am Handler, die der RolesGuard anschliessend mit dem Reflector ausliest und mit der Rolle des authentifizierten Benutzers vergleicht. Dieses Muster haelt die Autorisierungslogik zentral, waehrend einzelne Controller nur deklarativ markieren muessen, welche Rolle noetig ist.


// roles.decorator.ts — attaches role metadata to a handler
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

// roles.guard.ts — reads the metadata and compares it with the request user
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

interface RequestWithUser {
  user?: { roles: string[] };
}

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private readonly reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.get<string[]>('roles', context.getHandler());
    if (!requiredRoles || requiredRoles.length === 0) return true;

    const request = context.switchToHttp().getRequest<RequestWithUser>();
    const userRoles = request.user?.roles ?? [];
    return requiredRoles.some((role) => userRoles.includes(role));
  }
}

// orders.controller.ts — declarative usage
@Roles('admin')
@Get('reports')
getReports(): Promise<Report[]> {
  return this.ordersService.generateReports();
}

6. Interceptors und Exception Filters

Interceptors umschliessen Handler-Aufrufe und koennen sowohl vor als auch nach der Ausfuehrung eingreifen, aehnlich einem Aspect-Oriented-Programming-Muster. Typische Anwendungsfaelle sind Logging, Response-Transformation und Timeout-Handling. Ein Interceptor implementiert NestInterceptor und erhaelt ueber CallHandler einen RxJS-Observable-Stream, den er mit Operatoren wie map oder tap transformieren kann, ohne die zugrunde liegende Handler-Logik zu kennen.

Exception Filters uebernehmen die zentrale Fehlerbehandlung. Statt in jedem Controller try/catch-Bloecke zu wiederholen, faengt ein globaler ExceptionFilter alle nicht behandelten Exceptions ab und formt sie zu einer konsistenten JSON-Fehlerantwort. Bei TypeScript mit NestJS lassen sich Filter zusaetzlich auf konkrete Exception-Klassen einschraenken, etwa mit @Catch(HttpException), sodass unterschiedliche Fehlerarten unterschiedlich formatiert werden koennen, ohne die Typsicherheit zu verlieren.

Die Kombination aus Guards, Interceptors und Filters ergibt eine Pipeline, in der jede Verantwortung an der richtigen Stelle liegt: Guards entscheiden ueber Zugriff, Pipes validieren Eingaben, Interceptors transformieren Antworten, und Filter vereinheitlichen Fehler. Dieses Zusammenspiel ist einer der Hauptgruende, warum groessere Teams NestJS gegenueber unstrukturiertem Express-Code bevorzugen.

7. Konfiguration und Environment-Variablen typisieren

Rohe Environment-Variablen aus process.env sind in TypeScript immer vom Typ string | undefined, unabhaengig davon, ob sie tatsaechlich gesetzt sind. Das offizielle @nestjs/config-Modul loest dieses Problem mit einem ConfigService, der Werte ueber ein typisiertes Schema validiert und mit Standardwerten versieht. In Kombination mit Joi oder Zod zur Validierung faellt eine fehlende Pflichtvariable bereits beim Anwendungsstart auf, statt erst mitten im Betrieb einen Fehler zu verursachen.

Ein bewaehrtes Muster bei TypeScript mit NestJS ist eine eigene Konfigurationsklasse mit typisierten Gettern, die den generischen ConfigService<Record<string, unknown>> kapselt. So greifen andere Services nie direkt auf ungetypte Konfigurationswerte zu, sondern immer ueber eine Klasse, die bereits die korrekten Typen liefert und zentrale Validierung sicherstellt.


// configuration.ts — typed configuration factory with validation
import { registerAs } from '@nestjs/config';

export default registerAs('database', () => ({
  host: process.env.DB_HOST ?? 'localhost',
  port: parseInt(process.env.DB_PORT ?? '3306', 10),
  name: process.env.DB_NAME ?? 'app',
}));

// app-config.service.ts — typed wrapper around ConfigService
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class AppConfigService {
  constructor(private readonly config: ConfigService) {}

  get databaseHost(): string {
    return this.config.get<string>('database.host', { infer: true })!;
  }

  get databasePort(): number {
    return this.config.get<number>('database.port', { infer: true })!;
  }
}

8. Testing mit dem Nest Testing Module

NestJS liefert mit @nestjs/testing ein eigenes Testing Module, das den kompletten Dependency-Injection-Container fuer Tests nachbildet. Statt Klassen manuell zu instanziieren und Abhaengigkeiten von Hand einzusetzen, baut Test.createTestingModule() ein isoliertes Modul auf, in dem einzelne Provider gezielt mit Mocks ueberschrieben werden koennen. Das haelt Unit-Tests nah an der echten Anwendungsstruktur, ohne echte Datenbanken oder externe Dienste anzusprechen.

Fuer End-to-End-Tests startet Test.createTestingModule() die komplette Applikation inklusive HTTP-Server, sodass Requests mit supertest gegen echte Routen gesendet werden koennen, waehrend einzelne Provider wie Datenbank-Repositories weiterhin gemockt bleiben. Bei TypeScript mit NestJS bleibt der Vorteil bestehen, dass Mock-Objekte durch Interfaces typisiert sind, sodass ein falsch implementierter Mock schon beim Kompilieren auffaellt, nicht erst beim Testlauf.


// users.service.spec.ts — unit test with a typed mocked repository
import { Test } from '@nestjs/testing';
import { UsersService } from './users.service';
import { UsersRepository } from './users.repository';

describe('UsersService', () => {
  let service: UsersService;
  let repository: jest.Mocked<UsersRepository>;

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      providers: [
        UsersService,
        {
          provide: UsersRepository,
          useValue: { findById: jest.fn(), save: jest.fn() } satisfies Partial<UsersRepository>,
        },
      ],
    }).compile();

    service = module.get(UsersService);
    repository = module.get(UsersRepository);
  });

  it('throws when the user does not exist', async () => {
    repository.findById.mockResolvedValue(null);
    await expect(service.findOneOrFail('42')).rejects.toThrow();
  });
});

9. NestJS im Vergleich zu Express und Fastify

NestJS baut intern auf Express oder wahlweise Fastify auf, fuegt aber eine Architekturschicht mit Modulen, Dependency Injection und Decorators hinzu. Diese Schicht kostet etwas Overhead bei kleinen Projekten, zahlt sich aber in groesseren Teams durch erzwungene Konsistenz aus. Die folgende Tabelle zeigt, wo NestJS im Vergleich zu den zugrunde liegenden Frameworks steht.

Kriterium Express Fastify NestJS
Architektur-Vorgabe Keine, freie Struktur Keine, Plugin-basiert Module, DI, Decorators
TypeScript-Integration Nachtraeglich via @types Gut, mit Type Providern Nativ, Decorator-basiert
Dependency Injection Nicht eingebaut Nicht eingebaut Vollstaendig, hierarchisch
Einstiegshuerde Niedrig Niedrig bis mittel Mittel bis hoch
Rohe Requests pro Sekunde Referenzwert Hoeher als Express Wie darunterliegendes Framework, plus DI-Overhead

Wer ein kleines Skript oder einen einzelnen Microservice ohne komplexe Teamstruktur baut, faehrt mit Express oder Fastify oft schneller ans Ziel. Sobald mehrere Teams an einem Backend arbeiten und Konsistenz ueber Modulgrenzen hinweg wichtig wird, spielt TypeScript mit NestJS seine Staerken aus: erzwungene Modulgrenzen, testbare Dependency Injection und eine Struktur, die neue Entwickler schneller einarbeitet, weil sie ueberall demselben Muster folgt.

Mironsoft

TypeScript-Backends, NestJS-Architektur und typsichere APIs

Backend-Architektur, die mit dem Team mitwaechst?

Wir konzipieren und implementieren NestJS-Backends mit sauberer Modulstruktur, typsicherer Dependency Injection und vollstaendiger Testabdeckung, damit euer Node-Backend auch mit wachsendem Team wartbar bleibt.

Architektur-Review

Modulgrenzen, DI-Struktur und Skalierbarkeit eures NestJS-Projekts pruefen

Implementierung

Module, DTOs, Guards und Interceptors nach bewaehrten Patterns aufbauen

Testing-Setup

Unit- und E2E-Tests mit dem Nest Testing Module aufsetzen

10. Zusammenfassung

TypeScript mit NestJS loest ein Problem, das reine Express-Anwendungen typischerweise nicht loesen: eine erzwungene, konsistente Architektur ueber Modulgrenzen hinweg. Module kapseln fachliche Bereiche, Provider werden per Dependency Injection typsicher verkabelt, DTOs verbinden Laufzeitvalidierung mit Compile-Zeit-Typen, und Guards, Interceptors sowie Exception Filters trennen Zugriffskontrolle, Transformation und Fehlerbehandlung sauber voneinander.

Der Preis fuer diese Struktur ist eine hoehere Einstiegshuerde und mehr Boilerplate als bei einem minimalen Express-Server. Fuer kleine Skripte lohnt sich NestJS selten. Sobald mehrere Entwickler oder Teams an einem Backend arbeiten, ueberwiegt der Vorteil einer Architektur, die neue Mitglieder schneller einarbeitet, weil jeder Teil der Anwendung demselben, gut dokumentierten Muster folgt.

TypeScript mit NestJS — Das Wichtigste auf einen Blick

Architektur

Module kapseln fachliche Bereiche, Controller bleiben duenn, Services tragen die Logik.

Dependency Injection

Provider werden ueber Konstruktor-Typen automatisch verkabelt, Interfaces brauchen ein InjectionToken.

Validierung

DTOs mit class-validator kombinieren Laufzeitpruefung und Compile-Zeit-Typ in einer Klasse.

Testing

Das Nest Testing Module baut den DI-Container fuer isolierte Unit- und E2E-Tests nach.

11. FAQ: TypeScript mit NestJS

1Brauche ich TypeScript zwingend fuer NestJS?
Technisch geht auch JavaScript, aber die automatische Dependency Injection basiert auf Typinformationen aus emitDecoratorMetadata. Ohne TypeScript verliert man den zentralen Vorteil.
2Provider vs. Service?
Ein Service ist eine Klasse mit Geschaeftslogik. Provider ist der allgemeinere Begriff fuer alles per DI Bereitgestellte: Services, Repositories, Factories, Custom-Werte.
3Interface als Injection Token?
Interfaces existieren nur zur Compile-Zeit. Ein InjectionToken, meist ein Symbol, ersetzt das Interface als Laufzeit-Referenz fuer die Injection.
4Guards vs. Middleware?
Middleware laeuft vor dem Routing ohne ExecutionContext. Guards laufen danach, kennen Handler und Metadaten und behandeln HTTP, WebSocket und RPC einheitlich.
5Was macht die ValidationPipe?
Sie transformiert das Objekt in eine DTO-Instanz, prueft class-validator-Regeln und wirft bei Verstoss automatisch eine BadRequestException mit Fehlerliste.
6Wann REQUEST-Scope verwenden?
Fuer request-spezifische Daten wie den eingeloggten Benutzer. Der Standard-Scope bleibt als Singleton performanter, wenn kein request-spezifischer Zustand noetig ist.
7NestJS mit Fastify statt Express?
Ja, ueber den FastifyAdapter laeuft dieselbe Anwendung ohne Aenderung der Controller-Logik auf Fastify, meist mit hoeherem Durchsatz.
8Provider mit Datenbankzugriff testen?
Im Testing Module den echten Provider durch einen typisierten Mock ersetzen, etwa mit useValue oder jest.Mocked, der Rest der Modulstruktur bleibt unveraendert.
9Interceptor vs. Exception Filter?
Ein Interceptor umschliesst die normale Ausfuehrung. Ein Exception Filter greift nur bei einer geworfenen Exception ein und vereinheitlicht die Fehlerantwort.
10Lohnt sich NestJS fuer kleine Projekte?
Selten. Der Overhead zahlt sich vor allem bei mehreren Entwicklern aus. Fuer ein kleines Skript ist Express oder Fastify meist die schnellere Wahl.