Module sind das Herzstück der NestJS-Architektur und ermöglichen die Strukturierung von Anwendungen in logische, wiederverwendbare und testbare Einheiten. Sie organisieren verwandte Komponenten wie Controller, Services und Provider in kohärente Funktionsgruppen und definieren klare Grenzen zwischen verschiedenen Teilen der Anwendung.
Beginnen wir mit der Erstellung eines einfachen Moduls, um die
Grundkonzepte zu verstehen. Ein Modul in NestJS ist eine
TypeScript-Klasse, die mit dem @Module()-Dekorator
annotiert ist.
# Neues Modul über die CLI erstellen
nest generate module users
# oder kurz:
nest g module usersDies generiert folgende Datei:
// src/users/users.module.ts
import { Module } from '@nestjs/common';
@Module({})
export class UsersModule {}Ein leeres Modul ist jedoch noch nicht sehr nützlich. Lassen Sie uns einen Controller und Service hinzufügen:
# Controller und Service für das Users-Modul erstellen
nest g controller users
nest g service usersNach der Generierung sieht unser Modul folgendermaßen aus:
// src/users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}Damit unser neues Modul von der Anwendung erkannt wird, muss es in das Root-Modul importiert werden:
// src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UsersModule } from './users/users.module';
@Module({
imports: [UsersModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}Der @Module()-Dekorator akzeptiert ein
Konfigurationsobjekt mit verschiedenen Eigenschaften, die das Verhalten
des Moduls definieren.
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { UserRepository } from './user.repository';
import { User } from './entities/user.entity';
import { EmailService } from '../shared/services/email.service';
@Module({
// Module, die von diesem Modul benötigt werden
imports: [
TypeOrmModule.forFeature([User]),
],
// Controller, die in diesem Modul definiert sind
controllers: [UsersController],
// Provider (Services, Repositories, etc.), die in diesem Modul verfügbar sind
providers: [
UsersService,
UserRepository,
{
provide: 'EMAIL_SERVICE',
useClass: EmailService,
},
],
// Provider, die für andere Module verfügbar gemacht werden
exports: [
UsersService,
UserRepository,
],
})
export class UsersModule {}Eigenschaften des @Module() Dekorators:
Provider können auf verschiedene Weise definiert werden:
@Module({
providers: [
// Kurzform - Klasse als Token und Implementierung
UsersService,
// Vollständige Form
{
provide: UsersService,
useClass: UsersService,
},
// Factory Provider
{
provide: 'DATABASE_CONNECTION',
useFactory: async (configService: ConfigService) => {
const config = configService.get('database');
return createConnection(config);
},
inject: [ConfigService],
},
// Value Provider
{
provide: 'API_VERSION',
useValue: 'v1',
},
// Async Provider
{
provide: 'ASYNC_SERVICE',
useFactory: async () => {
const connection = await createConnection();
return new AsyncService(connection);
},
},
],
})
export class UsersModule {}NestJS-Module können voneinander abhängen und eine komplexe Hierarchie bilden. Das Verständnis dieser Abhängigkeiten ist entscheidend für eine saubere Architektur.
// Shared Module - wird von vielen anderen Modulen verwendet
@Module({
providers: [LoggerService, ConfigService],
exports: [LoggerService, ConfigService],
})
export class SharedModule {}
// Auth Module - abhängig von Users und Shared
@Module({
imports: [UsersModule, SharedModule],
providers: [AuthService, JwtService],
controllers: [AuthController],
exports: [AuthService],
})
export class AuthModule {}
// Users Module - abhängig von Shared
@Module({
imports: [SharedModule],
providers: [UsersService],
controllers: [UsersController],
exports: [UsersService],
})
export class UsersModule {}
// Products Module - abhängig von Auth (für Authentifizierung)
@Module({
imports: [AuthModule, SharedModule],
providers: [ProductsService],
controllers: [ProductsController],
})
export class ProductsModule {}Zirkuläre Abhängigkeiten zwischen Modulen können auftreten und sollten vermieden werden:
// Problematisch: Users importiert Auth, Auth importiert Users
// Lösung 1: Gemeinsame Funktionalität in separates Modul auslagern
@Module({
providers: [SharedUserService],
exports: [SharedUserService],
})
export class SharedUserModule {}
@Module({
imports: [SharedUserModule],
providers: [AuthService],
exports: [AuthService],
})
export class AuthModule {}
@Module({
imports: [SharedUserModule],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
// Lösung 2: forwardRef() verwenden (nur wenn unvermeidlich)
@Module({
imports: [forwardRef(() => AuthModule)],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}Module bieten mehrere entscheidende Vorteile für die Anwendungsarchitektur:
Module schaffen klare Grenzen zwischen verschiedenen Funktionsbereichen:
// Jedes Modul kapselt seine Logik
@Module({
providers: [
UserService, // Nur innerhalb UsersModule verfügbar
UserRepository, // Nur innerhalb UsersModule verfügbar
],
exports: [UserService], // Nur UserService ist für andere Module verfügbar
})
export class UsersModule {}Module können in verschiedenen Kontexten wiederverwendet werden:
// Logging-Modul kann in allen Anwendungen verwendet werden
@Module({
providers: [
{
provide: 'LOGGER',
useFactory: (config: ConfigService) => {
return new Logger(config.get('logLevel'));
},
inject: [ConfigService],
},
],
exports: ['LOGGER'],
})
export class LoggingModule {
static forRoot(options: LoggingOptions): DynamicModule {
return {
module: LoggingModule,
providers: [
{
provide: 'LOGGING_OPTIONS',
useValue: options,
},
],
};
}
}Module erleichtern das Testen durch Isolation von Abhängigkeiten:
// Test-Setup für UsersModule
describe('UsersModule', () => {
let module: TestingModule;
beforeEach(async () => {
module = await Test.createTestingModule({
imports: [UsersModule],
})
.overrideProvider(UserRepository)
.useValue(mockUserRepository)
.compile();
});
it('should be defined', () => {
expect(module).toBeDefined();
});
});Module können bei Bedarf geladen werden:
// Dynamisches Laden von Modulen
async function loadUserModule() {
const { UsersModule } = await import('./users/users.module');
return UsersModule;
}Module in NestJS folgen einem hierarchischen Muster, das der Anwendungsarchitektur entspricht:
AppModule (Root)
├── CoreModule (Singleton Services)
│ ├── ConfigModule
│ ├── DatabaseModule
│ └── LoggingModule
├── SharedModule (Wiederverwendbare Services)
│ ├── UtilsModule
│ ├── ValidationModule
│ └── CacheModule
└── FeatureModules (Geschäftslogik)
├── AuthModule
├── UsersModule
├── ProductsModule
└── OrdersModule
// src/app.module.ts
@Module({
imports: [
// Core Module - nur einmal importiert
CoreModule,
// Feature Module
AuthModule,
UsersModule,
ProductsModule,
OrdersModule,
// Conditional Imports basierend auf Environment
...(process.env.NODE_ENV === 'development' ? [DevToolsModule] : []),
],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes('*');
}
}Feature-Module organisieren geschäftsspezifische Funktionalitäten und sind die häufigste Art von Modulen in NestJS-Anwendungen.
// src/products/products.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ProductsController } from './controllers/products.controller';
import { ProductsService } from './services/products.service';
import { ProductRepository } from './repositories/product.repository';
import { Product } from './entities/product.entity';
import { Category } from './entities/category.entity';
import { ProductsResolver } from './resolvers/products.resolver';
import { CacheModule } from '@nestjs/cache-manager';
@Module({
imports: [
TypeOrmModule.forFeature([Product, Category]),
CacheModule.register({
ttl: 300, // 5 Minuten
max: 100, // Maximale Anzahl von Einträgen
}),
],
controllers: [ProductsController],
providers: [
ProductsService,
ProductRepository,
ProductsResolver,
{
provide: 'PRODUCT_CONFIG',
useValue: {
maxPrice: 10000,
defaultCurrency: 'EUR',
},
},
],
exports: [ProductsService, ProductRepository],
})
export class ProductsModule {}// src/products/products.module.ts
@Module({
imports: [
// Sub-Module für verschiedene Aspekte
ProductCatalogModule,
ProductInventoryModule,
ProductPricingModule,
ProductReviewsModule,
],
exports: [
ProductCatalogModule,
ProductInventoryModule,
ProductPricingModule,
],
})
export class ProductsModule {}
// src/products/catalog/product-catalog.module.ts
@Module({
providers: [ProductCatalogService],
controllers: [ProductCatalogController],
exports: [ProductCatalogService],
})
export class ProductCatalogModule {}Shared-Module enthalten wiederverwendbare Funktionalitäten, die von mehreren Feature-Modulen genutzt werden.
// src/shared/shared.module.ts
import { Global, Module } from '@nestjs/common';
import { EmailService } from './services/email.service';
import { FileUploadService } from './services/file-upload.service';
import { ValidationService } from './services/validation.service';
import { UtilsService } from './services/utils.service';
@Global() // Macht das Modul global verfügbar
@Module({
providers: [
EmailService,
FileUploadService,
ValidationService,
UtilsService,
],
exports: [
EmailService,
FileUploadService,
ValidationService,
UtilsService,
],
})
export class SharedModule {}// src/shared/shared.module.ts
@Module({})
export class SharedModule {
static forRoot(options: SharedModuleOptions): DynamicModule {
const providers = [UtilsService];
if (options.enableEmail) {
providers.push(EmailService);
}
if (options.enableFileUpload) {
providers.push(FileUploadService);
}
return {
module: SharedModule,
providers,
exports: providers,
global: options.global ?? false,
};
}
}
// Verwendung im AppModule
@Module({
imports: [
SharedModule.forRoot({
enableEmail: true,
enableFileUpload: true,
global: true,
}),
],
})
export class AppModule {}Core-Module enthalten Singleton-Services, die Application-weit verfügbar sein sollen und nur einmal instanziiert werden.
// src/core/core.module.ts
import { Global, Module, OnModuleInit } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { DatabaseService } from './services/database.service';
import { LoggerService } from './services/logger.service';
import { HealthService } from './services/health.service';
@Global()
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: ['.env.local', '.env'],
}),
TypeOrmModule.forRootAsync({
useFactory: (configService: ConfigService) => ({
type: 'postgres',
host: configService.get('DB_HOST'),
port: configService.get('DB_PORT'),
username: configService.get('DB_USERNAME'),
password: configService.get('DB_PASSWORD'),
database: configService.get('DB_NAME'),
autoLoadEntities: true,
synchronize: configService.get('NODE_ENV') !== 'production',
}),
inject: [ConfigService],
}),
],
providers: [DatabaseService, LoggerService, HealthService],
exports: [DatabaseService, LoggerService, HealthService],
})
export class CoreModule implements OnModuleInit {
constructor(
private readonly databaseService: DatabaseService,
private readonly logger: LoggerService,
) {}
async onModuleInit() {
await this.databaseService.checkConnection();
this.logger.log('Core module initialized', 'CoreModule');
}
}// src/core/core.module.ts
@Module({
providers: [
// Nur im Root Module importieren
{
provide: 'CORE_GUARD',
useFactory: () => {
if (CoreModule.initialized) {
throw new Error('CoreModule is already loaded. Import it only once in AppModule.');
}
CoreModule.initialized = true;
return true;
},
},
],
})
export class CoreModule {
private static initialized = false;
constructor(@Optional() @SkipSelf() parentModule: CoreModule) {
if (parentModule) {
throw new Error('CoreModule is already loaded. Import it only once in AppModule.');
}
}
}Dynamische Module ermöglichen die Konfiguration von Modulen zur Laufzeit und sind besonders nützlich für wiederverwendbare Bibliotheken.
// src/database/database.module.ts
import { DynamicModule, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
export interface DatabaseModuleOptions {
host: string;
port: number;
username: string;
password: string;
database: string;
entities: any[];
}
@Module({})
export class DatabaseModule {
static forRoot(options: DatabaseModuleOptions): DynamicModule {
return {
module: DatabaseModule,
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
...options,
}),
],
global: true,
};
}
static forFeature(entities: any[]): DynamicModule {
return {
module: DatabaseModule,
imports: [TypeOrmModule.forFeature(entities)],
exports: [TypeOrmModule],
};
}
}// src/config/config.module.ts
@Module({})
export class ConfigModule {
static forRootAsync(options: ConfigAsyncOptions): DynamicModule {
return {
module: ConfigModule,
imports: options.imports || [],
providers: [
{
provide: 'CONFIG_OPTIONS',
useFactory: options.useFactory,
inject: options.inject || [],
},
{
provide: 'CONFIG_SERVICE',
useFactory: async (configOptions: any) => {
const config = await loadConfig(configOptions);
return new ConfigService(config);
},
inject: ['CONFIG_OPTIONS'],
},
],
exports: ['CONFIG_SERVICE'],
global: options.isGlobal ?? false,
};
}
}
// Verwendung
@Module({
imports: [
ConfigModule.forRootAsync({
imports: [HttpModule],
useFactory: async (httpService: HttpService) => {
const remoteConfig = await httpService.get('/config').toPromise();
return remoteConfig.data;
},
inject: [HttpService],
isGlobal: true,
}),
],
})
export class AppModule {}// src/auth/auth.module.ts
export interface AuthModuleOptions {
jwtSecret: string;
jwtExpiresIn: string;
enableRefreshTokens?: boolean;
providers?: ('local' | 'google' | 'facebook')[];
}
@Module({})
export class AuthModule {
static forRoot(options: AuthModuleOptions): DynamicModule {
const providers = [
AuthService,
JwtStrategy,
{
provide: 'AUTH_OPTIONS',
useValue: options,
},
];
// Conditional providers based on options
if (options.enableRefreshTokens) {
providers.push(RefreshTokenService);
}
if (options.providers?.includes('google')) {
providers.push(GoogleStrategy);
}
if (options.providers?.includes('facebook')) {
providers.push(FacebookStrategy);
}
return {
module: AuthModule,
imports: [
JwtModule.register({
secret: options.jwtSecret,
signOptions: { expiresIn: options.jwtExpiresIn },
}),
],
providers,
controllers: [AuthController],
exports: [AuthService],
};
}
static forFeature(): DynamicModule {
return {
module: AuthModule,
providers: [AuthGuard],
exports: [AuthGuard],
};
}
}Eine durchdachte Modul-Hierarchie ist entscheidend für die Wartbarkeit und Skalierbarkeit von NestJS-Anwendungen.
AppModule (Root)
├── CoreModule (Infrastructure & Singletons)
│ ├── ConfigModule
│ ├── DatabaseModule
│ ├── LoggingModule
│ └── HealthModule
│
├── SharedModule (Common Utilities)
│ ├── ValidationModule
│ ├── CacheModule
│ ├── EmailModule
│ └── FileUploadModule
│
├── FeatureModules (Business Logic)
│ ├── AuthModule
│ │ ├── GuardsModule
│ │ ├── StrategiesModule
│ │ └── TokenModule
│ │
│ ├── UsersModule
│ │ ├── ProfileModule
│ │ ├── PreferencesModule
│ │ └── RolesModule
│ │
│ ├── ProductsModule
│ │ ├── CatalogModule
│ │ ├── InventoryModule
│ │ ├── PricingModule
│ │ └── ReviewsModule
│ │
│ └── OrdersModule
│ ├── PaymentModule
│ ├── ShippingModule
│ └── InvoiceModule
│
└── IntegrationModules (External Services)
├── PaymentGatewayModule
├── NotificationModule
└── AnalyticsModule
// Layer 1: Infrastructure
@Module({
imports: [ConfigModule, DatabaseModule],
exports: [ConfigModule, DatabaseModule],
})
export class InfrastructureModule {}
// Layer 2: Domain Services
@Module({
imports: [InfrastructureModule],
providers: [UserDomainService, ProductDomainService],
exports: [UserDomainService, ProductDomainService],
})
export class DomainModule {}
// Layer 3: Application Services
@Module({
imports: [DomainModule],
providers: [UserApplicationService, ProductApplicationService],
exports: [UserApplicationService, ProductApplicationService],
})
export class ApplicationModule {}
// Layer 4: Presentation
@Module({
imports: [ApplicationModule],
controllers: [UsersController, ProductsController],
})
export class PresentationModule {}src/users/
├── controllers/
│ ├── users.controller.ts
│ ├── user-profile.controller.ts
│ └── user-admin.controller.ts
├── services/
│ ├── users.service.ts
│ ├── user-profile.service.ts
│ └── user-validation.service.ts
├── repositories/
│ ├── user.repository.ts
│ └── user-profile.repository.ts
├── dto/
│ ├── create-user.dto.ts
│ ├── update-user.dto.ts
│ ├── user-profile.dto.ts
│ └── user-query.dto.ts
├── entities/
│ ├── user.entity.ts
│ └── user-profile.entity.ts
├── interfaces/
│ ├── user.interface.ts
│ └── user-repository.interface.ts
├── guards/
│ └── user-ownership.guard.ts
├── pipes/
│ └── user-validation.pipe.ts
├── decorators/
│ └── current-user.decorator.ts
├── tests/
│ ├── users.service.spec.ts
│ ├── users.controller.spec.ts
│ └── user.repository.spec.ts
└── users.module.ts
// src/users/users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CacheModule } from '@nestjs/cache-manager';
// Controllers
import { UsersController } from './controllers/users.controller';
import { UserProfileController } from './controllers/user-profile.controller';
import { UserAdminController } from './controllers/user-admin.controller';
// Services
import { UsersService } from './services/users.service';
import { UserProfileService } from './services/user-profile.service';
import { UserValidationService } from './services/user-validation.service';
// Repositories
import { UserRepository } from './repositories/user.repository';
import { UserProfileRepository } from './repositories/user-profile.repository';
// Entities
import { User } from './entities/user.entity';
import { UserProfile } from './entities/user-profile.entity';
// Guards
import { UserOwnershipGuard } from './guards/user-ownership.guard';
// External Dependencies
import { AuthModule } from '../auth/auth.module';
import { SharedModule } from '../shared/shared.module';
@Module({
imports: [
TypeOrmModule.forFeature([User, UserProfile]),
CacheModule.register({
ttl: 300,
max: 1000,
}),
AuthModule.forFeature(), // Importiert nur AuthGuard
SharedModule, // Globales Modul
],
controllers: [
UsersController,
UserProfileController,
UserAdminController,
],
providers: [
// Services
UsersService,
UserProfileService,
UserValidationService,
// Repositories
UserRepository,
UserProfileRepository,
// Guards
UserOwnershipGuard,
// Custom Providers
{
provide: 'USER_CONFIG',
useValue: {
maxProfileImageSize: 2 * 1024 * 1024, // 2MB
allowedImageTypes: ['jpg', 'jpeg', 'png'],
},
},
// Factory Provider
{
provide: 'USER_CACHE_KEY_GENERATOR',
useFactory: (configService: ConfigService) => {
const prefix = configService.get('CACHE_PREFIX');
return (userId: string) => `${prefix}:user:${userId}`;
},
inject: [ConfigService],
},
],
exports: [
UsersService,
UserRepository,
UserValidationService,
],
})
export class UsersModule {}Das Verständnis von Import- und Export-Mechanismen ist entscheidend für eine saubere Modul-Architektur.
// Standard Import
@Module({
imports: [UsersModule],
})
export class AppModule {}
// Conditional Import
@Module({
imports: [
UsersModule,
...(process.env.NODE_ENV === 'development' ? [DevToolsModule] : []),
...(process.env.ENABLE_ADMIN === 'true' ? [AdminModule] : []),
],
})
export class AppModule {}
// Dynamic Import mit Konfiguration
@Module({
imports: [
DatabaseModule.forRoot({
type: 'postgres',
host: process.env.DB_HOST,
}),
CacheModule.forRoot({
store: 'redis',
host: process.env.REDIS_HOST,
}),
],
})
export class AppModule {}// Selective Export
@Module({
providers: [
UserService,
UserRepository,
UserValidationService,
InternalUserService, // Nicht exportiert
],
exports: [
UserService,
UserRepository,
// UserValidationService nicht exportiert
],
})
export class UsersModule {}
// Re-Export von importierten Modulen
@Module({
imports: [TypeOrmModule.forFeature([User])],
providers: [UserService],
exports: [
UserService,
TypeOrmModule, // Re-export für andere Module
],
})
export class UsersModule {}
// Token-basierter Export
@Module({
providers: [
{
provide: 'USER_SERVICE',
useClass: UserService,
},
],
exports: ['USER_SERVICE'],
})
export class UsersModule {}// Aggregator Module Pattern
@Module({
imports: [
UserCoreModule,
UserProfileModule,
UserPreferencesModule,
UserRolesModule,
],
exports: [
UserCoreModule,
UserProfileModule,
UserPreferencesModule,
UserRolesModule,
],
})
export class UsersModule {}
// Facade Module Pattern
@Module({
imports: [UsersModule, ProductsModule, OrdersModule],
providers: [ECommerceService], // Vereint Funktionalitäten
exports: [ECommerceService],
})
export class ECommerceModule {}Lassen Sie uns ein vollständiges Feature-Modul für ein Blog-System entwickeln, das alle besprochenen Konzepte demonstriert.
src/blog/
├── controllers/
│ ├── posts.controller.ts
│ ├── comments.controller.ts
│ └── categories.controller.ts
├── services/
│ ├── posts.service.ts
│ ├── comments.service.ts
│ ├── categories.service.ts
│ └── blog-search.service.ts
├── repositories/
│ ├── post.repository.ts
│ ├── comment.repository.ts
│ └── category.repository.ts
├── dto/
│ ├── create-post.dto.ts
│ ├── update-post.dto.ts
│ ├── create-comment.dto.ts
│ ├── post-query.dto.ts
│ └── pagination.dto.ts
├── entities/
│ ├── post.entity.ts
│ ├── comment.entity.ts
│ └── category.entity.ts
├── guards/
│ ├── post-ownership.guard.ts
│ └── post-published.guard.ts
├── interceptors/
│ └── post-cache.interceptor.ts
├── pipes/
│ └── post-validation.pipe.ts
├── interfaces/
│ ├── blog.interface.ts
│ └── search-result.interface.ts
└── blog.module.ts
// src/blog/entities/post.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany, CreateDateColumn, UpdateDateColumn } from 'typeorm';
import { User } from '../../users/entities/user.entity';
import { Category } from './category.entity';
import { Comment } from './comment.entity';
@Entity('posts')
export class Post {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
title: string;
@Column()
slug: string;
@Column('text')
content: string;
@Column('text', { nullable: true })
excerpt: string;
@Column({ default: false })
published: boolean;
@Column({ name: 'featured_image', nullable: true })
featuredImage: string;
@Column('simple-array', { nullable: true })
tags: string[];
@ManyToOne(() => User, user => user.posts)
author: User;
@ManyToOne(() => Category, category => category.posts)
category: Category;
@OneToMany(() => Comment, comment => comment.post)
comments: Comment[];
@CreateDateColumn({ name: 'created_at' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at' })
updatedAt: Date;
}
// src/blog/entities/comment.entity.ts
@Entity('comments')
export class Comment {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column('text')
content: string;
@Column({ default: false })
approved: boolean;
@ManyToOne(() => User, user => user.comments)
author: User;
@ManyToOne(() => Post, post => post.comments, { onDelete: 'CASCADE' })
post: Post;
@ManyToOne(() => Comment, comment => comment.replies, { nullable: true })
parent: Comment;
@OneToMany(() => Comment, comment => comment.parent)
replies: Comment[];
@CreateDateColumn({ name: 'created_at' })
createdAt: Date;
}
// src/blog/entities/category.entity.ts
@Entity('categories')
export class Category {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ unique: true })
name: string;
@Column({ unique: true })
slug: string;
@Column('text', { nullable: true })
description: string;
@OneToMany(() => Post, post => post.category)
posts: Post[];
}// src/blog/dto/create-post.dto.ts
import { IsString, IsBoolean, IsOptional, IsArray, IsUUID, MinLength, MaxLength } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class CreatePostDto {
@ApiProperty({ description: 'Post title', minLength: 5, maxLength: 100 })
@IsString()
@MinLength(5)
@MaxLength(100)
title: string;
@ApiProperty({ description: 'Post content' })
@IsString()
@MinLength(10)
content: string;
@ApiPropertyOptional({ description: 'Post excerpt' })
@IsOptional()
@IsString()
@MaxLength(255)
excerpt?: string;
@ApiPropertyOptional({ description: 'Publication status', default: false })
@IsOptional()
@IsBoolean()
published?: boolean;
@ApiPropertyOptional({ description: 'Featured image URL' })
@IsOptional()
@IsString()
featuredImage?: string;
@ApiPropertyOptional({ description: 'Post tags' })
@IsOptional()
@IsArray()
@IsString({ each: true })
tags?: string[];
@ApiProperty({ description: 'Category ID' })
@IsUUID()
categoryId: string;
}
// src/blog/dto/post-query.dto.ts
import { IsOptional, IsString, IsBoolean, IsInt, Min, Max } from 'class-validator';
import { Type } from 'class-transformer';
import { ApiPropertyOptional } from '@nestjs/swagger';
export class PostQueryDto {
@ApiPropertyOptional({ description: 'Search term' })
@IsOptional()
@IsString()
search?: string;
@ApiPropertyOptional({ description: 'Category slug' })
@IsOptional()
@IsString()
category?: string;
@ApiPropertyOptional({ description: 'Tag filter' })
@IsOptional()
@IsString()
tag?: string;
@ApiPropertyOptional({ description: 'Author ID' })
@IsOptional()
@IsString()
author?: string;
@ApiPropertyOptional({ description: 'Publication status' })
@IsOptional()
@IsBoolean()
@Type(() => Boolean)
published?: boolean;
@ApiPropertyOptional({ description: 'Page number', minimum: 1, default: 1 })
@IsOptional()
@IsInt()
@Min(1)
@Type(() => Number)
page?: number = 1;
@ApiPropertyOptional({ description: 'Items per page', minimum: 1, maximum: 100, default: 10 })
@IsOptional()
@IsInt()
@Min(1)
@Max(100)
@Type(() => Number)
limit?: number = 10;
}// src/blog/services/posts.service.ts
import { Injectable, NotFoundException, ForbiddenException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Post } from '../entities/post.entity';
import { CreatePostDto } from '../dto/create-post.dto';
import { UpdatePostDto } from '../dto/update-post.dto';
import { PostQueryDto } from '../dto/post-query.dto';
import { User } from '../../users/entities/user.entity';
import { BlogSearchService } from './blog-search.service';
import { CacheService } from '../../shared/services/cache.service';
@Injectable()
export class PostsService {
constructor(
@InjectRepository(Post)
private readonly postRepository: Repository<Post>,
private readonly searchService: BlogSearchService,
private readonly cacheService: CacheService,
) {}
async create(createPostDto: CreatePostDto, author: User): Promise<Post> {
const slug = this.generateSlug(createPostDto.title);
const post = this.postRepository.create({
...createPostDto,
slug,
author,
});
const savedPost = await this.postRepository.save(post);
// Index for search
await this.searchService.indexPost(savedPost);
// Invalidate relevant caches
await this.cacheService.del(`posts:category:${createPostDto.categoryId}`);
return savedPost;
}
async findAll(queryDto: PostQueryDto): Promise<{ posts: Post[]; total: number }> {
const cacheKey = `posts:query:${JSON.stringify(queryDto)}`;
const cached = await this.cacheService.get(cacheKey);
if (cached) {
return cached;
}
const queryBuilder = this.postRepository
.createQueryBuilder('post')
.leftJoinAndSelect('post.author', 'author')
.leftJoinAndSelect('post.category', 'category')
.leftJoinAndSelect('post.comments', 'comments');
// Apply filters
if (queryDto.published !== undefined) {
queryBuilder.andWhere('post.published = :published', { published: queryDto.published });
}
if (queryDto.category) {
queryBuilder.andWhere('category.slug = :category', { category: queryDto.category });
}
if (queryDto.tag) {
queryBuilder.andWhere(':tag = ANY(post.tags)', { tag: queryDto.tag });
}
if (queryDto.author) {
queryBuilder.andWhere('author.id = :author', { author: queryDto.author });
}
if (queryDto.search) {
queryBuilder.andWhere(
'(post.title ILIKE :search OR post.content ILIKE :search OR post.excerpt ILIKE :search)',
{ search: `%${queryDto.search}%` }
);
}
// Pagination
const skip = (queryDto.page - 1) * queryDto.limit;
queryBuilder.skip(skip).take(queryDto.limit);
// Ordering
queryBuilder.orderBy('post.createdAt', 'DESC');
const [posts, total] = await queryBuilder.getManyAndCount();
const result = { posts, total };
// Cache for 5 minutes
await this.cacheService.set(cacheKey, result, 300);
return result;
}
async findOne(id: string): Promise<Post> {
const cacheKey = `post:${id}`;
const cached = await this.cacheService.get(cacheKey);
if (cached) {
return cached;
}
const post = await this.postRepository.findOne({
where: { id },
relations: ['author', 'category', 'comments', 'comments.author'],
});
if (!post) {
throw new NotFoundException(`Post with ID ${id} not found`);
}
await this.cacheService.set(cacheKey, post, 600); // 10 minutes
return post;
}
async update(id: string, updatePostDto: UpdatePostDto, user: User): Promise<Post> {
const post = await this.findOne(id);
if (post.author.id !== user.id && !user.roles.includes('admin')) {
throw new ForbiddenException('You can only update your own posts');
}
if (updatePostDto.title && updatePostDto.title !== post.title) {
updatePostDto.slug = this.generateSlug(updatePostDto.title);
}
Object.assign(post, updatePostDto);
const updatedPost = await this.postRepository.save(post);
// Update search index
await this.searchService.updatePost(updatedPost);
// Invalidate caches
await this.cacheService.del(`post:${id}`);
await this.cacheService.del(`posts:category:${post.category.id}`);
return updatedPost;
}
async remove(id: string, user: User): Promise<void> {
const post = await this.findOne(id);
if (post.author.id !== user.id && !user.roles.includes('admin')) {
throw new ForbiddenException('You can only delete your own posts');
}
await this.postRepository.remove(post);
// Remove from search index
await this.searchService.removePost(id);
// Invalidate caches
await this.cacheService.del(`post:${id}`);
await this.cacheService.del(`posts:category:${post.category.id}`);
}
private generateSlug(title: string): string {
return title
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.trim();
}
}// src/blog/blog.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CacheModule } from '@nestjs/cache-manager';
// Controllers
import { PostsController } from './controllers/posts.controller';
import { CommentsController } from './controllers/comments.controller';
import { CategoriesController } from './controllers/categories.controller';
// Services
import { PostsService } from './services/posts.service';
import { CommentsService } from './services/comments.service';
import { CategoriesService } from './services/categories.service';
import { BlogSearchService } from './services/blog-search.service';
// Entities
import { Post } from './entities/post.entity';
import { Comment } from './entities/comment.entity';
import { Category } from './entities/category.entity';
// Guards
import { PostOwnershipGuard } from './guards/post-ownership.guard';
import { PostPublishedGuard } from './guards/post-published.guard';
// External modules
import { UsersModule } from '../users/users.module';
import { AuthModule } from '../auth/auth.module';
import { SharedModule } from '../shared/shared.module';
@Module({
imports: [
TypeOrmModule.forFeature([Post, Comment, Category]),
CacheModule.register({
ttl: 300, // 5 minutes default
max: 1000,
}),
UsersModule, // For User entity relationships
AuthModule.forFeature(), // For authentication guards
SharedModule, // For shared services like CacheService
],
controllers: [
PostsController,
CommentsController,
CategoriesController,
],
providers: [
// Services
PostsService,
CommentsService,
CategoriesService,
BlogSearchService,
// Guards
PostOwnershipGuard,
PostPublishedGuard,
// Configuration
{
provide: 'BLOG_CONFIG',
useValue: {
postsPerPage: 10,
maxPostLength: 50000,
allowedImageTypes: ['jpg', 'jpeg', 'png', 'webp'],
enableComments: true,
enableSearch: true,
},
},
],
exports: [
PostsService,
CategoriesService,
BlogSearchService,
],
})
export class BlogModule {}Dieses umfassende Beispiel zeigt, wie Module in NestJS als organisatorische Einheiten fungieren, die verwandte Funktionalitäten kapseln, klare Abhängigkeiten definieren und eine saubere Architektur fördern. Die modulare Struktur ermöglicht es, komplexe Anwendungen in überschaubare, testbare und wartbare Komponenten zu unterteilen.