Was ist NestJS?
NestJS ist ein progressives Node.js Framework, das Struktur in serverseitiges JavaScript bringt. Während Express Ihnen lediglich Primitiven bereitstellt und Ihnen die Wahl überlässt, bietet NestJS eine fertige Architektur: Module, Controller, Provider, Decorators und einen Dependency Injection Container. Es lehnt sich stark an Angular sowie an Enterprise-Java-Frameworks an und läuft entweder auf Express oder Fastify.
Dieser „opinionated“ Ansatz ist das eigentliche Wertversprechen. In einem fünfköpfigen Team, das eine große API entwickelt, sind die schwierigsten Probleme selten HTTP-bezogen. Es geht vielmehr um Benennungen, Grenzen, Testing und die Beibehaltung konsistenter Patterns bei wachsender Codebasis. NestJS löst diese Probleme durch Konventionen, die man entweder akzeptiert oder ersetzt. Zudem liefert es das gesamte „Plumbing“ mit – Validierung, Guards, Interceptors, Konfiguration und Testing-Utilities –, sodass Sie diese nicht selbst zusammenbauen müssen.
Module definieren die Grenzen
Alles in NestJS ist in Modulen organisiert. Ein Modul ist eine Klasse, die mit @Module dekoriert ist und ihre Controller, ihre Provider sowie die Exporte für den Rest der Anwendung definiert.
@Module({
imports: [DatabaseModule],
controllers: [CatsController],
providers: [CatsService],
exports: [CatsService],
})
export class CatsModule {}
Über imports werden andere Module eingebunden, deren exportierte Provider benötigt werden. providers sind die Klassen, die dieses Modul besitzt. exports ist die öffentliche API: Nur das, was ein Modul exportiert, kann an anderer Stelle injiziert werden. Dadurch werden Abhängigkeiten explizit – wenn OrdersModule CatsService benötigt, muss es CatsModule importieren, und der compiler-ähnliche Container meldet es Ihnen, wenn etwas nicht aufgelöst werden kann.
Das Root-AppModule importiert jedes Feature-Modul. Feature-Module können in Shared- oder Core-Module gruppiert werden, und @Global() markiert ein Modul, das überall verfügbar sein soll, ohne dass es wiederholt importiert werden muss. Verwenden Sie globale Module sparsam für echte querschnittliche Infrastruktur wie Konfiguration oder Logging.
Controller: Routing über Decorators
Ein Controller bildet HTTP-Requests auf Methoden ab. Der Class-Decorator legt den Basis-Pfad fest, während Method-Decorators das Verb und den Unterpfad definieren.
@Controller("cats")
export class CatsController {
constructor(private readonly catsService: CatsService) {}
@Get()
findAll() {
return this.catsService.findAll();
}
@Get(":id")
findOne(@Param("id") id: string) {
return this.catsService.findOne(id);
}
@Post()
create(@Body() dto: CreateCatDto) {
return this.catsService.create(dto);
}
}
Parameter-Decorators übernehmen die Extraktion: @Param, @Query, @Body, @Headers, @Req und @Res. Der Handler gibt einen einfachen Wert oder ein Promise zurück, welches NestJS mit dem entsprechenden Status als JSON serialisiert. Wirft man eine HttpException wie zum Beispiel NotFoundException, wandelt das Framework diese in eine korrekte Error-Response um.
Controller sollten “thin” bleiben. Sie übersetzen HTTP-Anfragen in einen Methodenaufruf und mehr nicht. Die gesamte Logik gehört in Provider; so bleibt der Controller testbar und die HTTP-Belange isoliert.
Provider und Dependency Injection
Ein Provider ist jede Klasse, die injiziert werden kann. Markiere sie mit @Injectable() und deklariere sie in einem Modul, und der Container erledigt den Rest.
@Injectable()
export class CatsService {
constructor(private readonly db: DatabaseService) {}
findAll() {
return this.db.query("SELECT * FROM cats");
}
}
Die Injection erfolgt über den Konstruktor und anhand des Typs. Der Container liest die ausgegebenen Metadaten, löst jeden Parameter auf und übergibt die Instanz. Wenn der Wert keine Klasse ist, kannst du auch eine Injection per Token vornehmen, indem du @Inject("CONFIG") zusammen mit einem passenden Provider verwendest.
Provider können durch Factories, Werte und Aliase angepasst werden. Ein Factory-Provider ist der übliche Weg, um Dinge zu injizieren, die ein asynchrones Setup benötigen, wie zum Beispiel eine Datenbankverbindung:
@Module({
providers: [
{
provide: "DATABASE",
useFactory: async (config: ConfigService) => {
return createPool(config.getOrThrow("DATABASE_URL"));
},
inject: [ConfigService],
},
],
exports: ["DATABASE"],
})
export class DatabaseModule {}
Auch der Scope ist entscheidend. Der Standard-Singleton-Scope teilt eine einzige Instanz über die gesamte App hinweg, was für zustandslose Services ideal ist. Der REQUEST Scope erstellt eine neue Instanz pro Request und ist nützlich für request-spezifische Kontexte, kostet jedoch Performance, da er den gesamten Dependency-Graph nach oben durchläuft.
DTOs, Pipes und Validierung
Die Validierung von Inputs ist ein zentrales Anliegen. Ein DTO (Data Transfer Object) ist eine Klasse, die die Struktur eines Requests beschreibt und mit class-validator-Regeln dekoriert wird.
export class CreateCatDto {
@IsString()
@MinLength(1)
name: string;
@IsInt()
@Min(0)
age: number;
}
Eine Pipe transformiert oder validiert den Wert, bevor er den Handler erreicht. Aktivieren Sie die integrierte ValidationPipe global, und jedes dekorierte DTO wird automatisch geprüft:
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
);
whitelist entfernt unbekannte Eigenschaften, forbidNonWhitelisted lehnt diese komplett ab und transform konvertiert Payloads in tatsächliche DTO-Instanzen, sodass die Typen zur Laufzeit real sind. Ungültige Inputs erzeugen eine strukturierte 400, ohne dass eine einzige Zeile Validierungscode im Handler geschrieben werden muss. Pipes können zudem pro Parameter oder pro Controller mittels @UsePipes angewendet werden.
Guards, Interceptors und Filter
Diese drei Bausteine decken die meisten querschnittlichen Belange (cross-cutting concerns) ab und werden in einer festgelegten Reihenfolge ausgeführt.
Guards werden vor dem Handler ausgeführt und entscheiden, ob die Anfrage fortgesetzt wird.
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const roles = this.reflector.get<string[]>("roles", context.getHandler());
if (!roles) return true;
const { user } = context.switchToHttp().getRequest();
return roles.includes(user?.role);
}
}
Interceptors umschließen den Handler-Aufruf und können das Ergebnis transformieren, die Zeit messen oder Caching hinzufügen.
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const start = Date.now();
return next.handle().pipe(
tap(() => console.log(`took ${Date.now() - start}ms`)),
);
}
}
Exception Filter fangen geworfene Fehler ab und formatieren die Antwort. Ohne einen eigenen Filter nutzt NestJS einen sinnvollen Standard; ein benutzerdefinierter Filter sorgt dafür, dass die Fehlerstrukturen über die gesamte API hinweg konsistent bleiben.
@Catch(HttpException)
export class HttpErrorFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const response = host.switchToHttp().getResponse();
const status = exception.getStatus();
response.status(status).json({
statusCode: status,
message: exception.message,
timestamp: new Date().toISOString(),
});
}
}
Registriere sie global, pro Controller oder pro Route. Globale Guards, Interceptors und Filter sind der Weg, wie eine ausgereifte NestJS-App Authentifizierung, Observability und Fehlerbehandlung zentralisiert.
Konfiguration und Umgebung
Das @nestjs/config Paket kapselt Umgebungsvariablen und macht sie injizierbar. Importiere es einmalig als globales Modul.
@Module({
imports: [ConfigModule.forRoot({ isGlobal: true })],
})
export class AppModule {}
Injiziere anschließend ConfigService überall dort, wo es benötigt wird:
@Injectable()
export class DatabaseService {
constructor(private readonly config: ConfigService) {}
connect() {
const url = this.config.getOrThrow<string>("DATABASE_URL");
return createPool(url);
}
}
Verwende für erforderliche Werte vorzugsweise getOrThrow, damit eine fehlende Variable bereits beim Start und nicht erst bei der ersten Anfrage zu einem Fehler führt. Kombiniere dies mit einem Validierungsschema, sodass die Konfiguration einmalig geprüft wird, und halte Secrets vollständig aus dem Repository fern.
Testen mit TestingModule
Die Testing-Utilities von NestJS erstellen einen echten Container in Isolation. Dabei deklarieren Sie nur die zu testenden Provider und überschreiben deren Abhängigkeiten anschließend mit Mocks.
describe("CatsController", () => {
let controller: CatsController;
let service: CatsService;
beforeEach(async () => {
const module = await Test.createTestingModule({
controllers: [CatsController],
providers: [CatsService],
}).compile();
controller = module.get(CatsController);
service = module.get(CatsService);
});
it("returns all cats", () => {
jest.spyOn(service, "findAll").mockReturnValue([]);
expect(controller.findAll()).toEqual([]);
});
});
Für End-to-End-Tests bauen Sie die vollständige App auf und verwenden supertest gegen app.getHttpServer(). Da derselbe DI-Container genutzt wird wie in der Produktion, prüfen diese Tests Guards, Pipes und Filter exakt so, wie sie im Deployment ausgeführt werden.
Best Practices
- Halten Sie Controller schlank und lagern Sie die gesamte Logik in injectable Services aus.
- Ziehen Sie Modulgrenzen um Features und exportieren Sie nur das, was andere Module benötigen.
- Validieren Sie jede Anfrage mit einem DTO und einem globalen
ValidationPipe. - Nutzen Sie Guards für die Authentifizierung und Autorisierung sowie Interceptors für die Gestaltung von Antworten.
- Registrieren Sie globale Exception Filter, damit Fehlerantworten ein einheitliches Format haben.
- Injizieren Sie Konfigurationen, anstatt
process.envdirekt auszulesen. - Bevorzugen Sie den Singleton Scope; nutzen Sie den Request Scope nur, wenn es wirklich notwendig ist.
- Testen Sie mit
TestingModuleund überschreiben Sie Abhängigkeiten, anstatt echte Services aufzurufen.
Häufige Fehler
- Provider mit
newinstanziieren, anstatt sie zu injizieren, was den Container beschädigt. - Vergessen, einen Provider zu einem Modul hinzuzufügen, und anschließend einen “Nest can’t resolve dependencies”-Fehler suchen.
- Ein Modul importieren, aber vergessen, den Provider, den es teilen soll, zu
export. - Business-Logik in Controllern platzieren, wodurch diese nicht mehr unit-testbar sind.
- Überall Request Scope verwenden und so unbemerkt die Performance verschlechtern.
@Body()ohne ein DTO und eine Validation Pipe vertrauen.- Die Fehlerformatierung an verschiedenen Stellen verteilen, anstatt sie zentral in einem Filter zu bündeln.
- Module als bloße Ordner betrachten, anstatt sie als Dependency Boundaries zu behandeln.
Wie geht es weiter?
NestJS ist der strukturierteste Weg, um ein Node.js Backend zu bauen, und basiert direkt auf Express oder Fastify. Lesen Sie diese Guides, um die HTTP-Layer zu verstehen, die Sie hier abstrahieren. Fundierte TypeScript-Kenntnisse sind essenziell, da Decorators und Dependency Injection stark auf Typen setzen, und die Node.js basics erklären die Runtime, auf der alles läuft. Bauen Sie anschließend ein Feature-Modul komplett end-to-end: Controller, Service, DTO und Test.