Zum Inhalt springen

NestJS

Progressives TypeScript-first Node.js-Framework zum Erstellen skalierbarer serverseitiger Anwendungen.

BefehlBeschreibung
npm i -g @nestjs/cliNestJS-CLI global installieren
nest new project-nameNeues NestJS-Projekt erstellen
nest new project-name --strictProjekt mit striktem TypeScript erstellen
nest new project-name -p pnpmProjekt mit pnpm erstellen
nest new project-name -p yarnProjekt mit Yarn erstellen
BefehlBeschreibung
npm run start:devEntwicklungsserver mit Hot Reload starten
npm run start:debugMit Debug-Modus und Hot Reload starten
npm run start:prodProduktionsserver starten
npm run buildProjekt bauen
npm run testUnit-Tests ausführen
npm run test:watchTests im Watch-Modus ausführen
npm run test:covTests mit Coverage-Bericht ausführen
npm run test:e2eEnd-to-End-Tests ausführen
npm run lintQuellcode linten
src/
├── app.controller.ts       # Root-Controller
├── app.controller.spec.ts  # Controller-Unit-Test
├── app.module.ts           # Root-Modul
├── app.service.ts          # Root-Service
├── main.ts                 # Anwendungs-Einstiegspunkt
├── users/
│   ├── users.module.ts     # Feature-Modul
│   ├── users.controller.ts # Feature-Controller
│   ├── users.service.ts    # Feature-Service
│   ├── dto/
│   │   ├── create-user.dto.ts
│   │   └── update-user.dto.ts
│   ├── entities/
│   │   └── user.entity.ts
│   └── users.controller.spec.ts
├── auth/
│   ├── auth.module.ts
│   ├── auth.guard.ts
│   ├── auth.service.ts
│   └── strategies/
│       └── jwt.strategy.ts
└── common/
    ├── filters/
    │   └── http-exception.filter.ts
    ├── interceptors/
    │   └── logging.interceptor.ts
    └── pipes/
        └── validation.pipe.ts
BefehlBeschreibung
nest generate module usersNeues Modul generieren
nest generate controller usersNeuen Controller generieren
nest generate service usersNeuen Service generieren
nest generate resource usersVollständige CRUD-Resource generieren (Modul + Controller + Service + DTOs)
nest generate guard authAuth-Guard generieren
nest generate pipe validationValidierungs-Pipe generieren
nest generate interceptor loggingInterceptor generieren
nest generate middleware loggerMiddleware generieren
nest generate filter http-exceptionException-Filter generieren
nest generate decorator rolesBenutzerdefinierten Decorator generieren
nest generate class user.entityEinfache Klasse generieren
nest generate interface userInterface generieren
nest g res users --no-specResource ohne Testdateien generieren
nest g mo users --flatOhne Unterordner generieren
nest infoProjektinformationen und Abhängigkeiten anzeigen
# Vollständige CRUD-Resource (häufigster Workflow)
nest g resource users

# CLI-Abfragen:
# ? What transport layer do you use? REST API
# ? Would you like to generate CRUD entry points? Yes

# Erstellt:
# src/users/users.module.ts
# src/users/users.controller.ts
# src/users/users.service.ts
# src/users/dto/create-user.dto.ts
# src/users/dto/update-user.dto.ts
# src/users/entities/user.entity.ts
# src/users/users.controller.spec.ts
# Aktualisiert: src/app.module.ts (importiert UsersModule)
BefehlBeschreibung
@Module({ imports: [UsersModule] })Anderes Modul importieren
@Module({ controllers: [UsersController] })Controller registrieren
@Module({ providers: [UsersService] })Provider/Services registrieren
@Module({ exports: [UsersService] })Provider für andere Module exportieren
@Global()Modul global verfügbar machen (sparsam verwenden)
import { Module, DynamicModule } from '@nestjs/common'

@Module({})
export class DatabaseModule {
  static forRoot(options: DatabaseOptions): DynamicModule {
    return {
      module: DatabaseModule,
      global: true,
      providers: [
        { provide: 'DATABASE_OPTIONS', useValue: options },
        DatabaseService,
      ],
      exports: [DatabaseService],
    }
  }

  static forRootAsync(options: DatabaseAsyncOptions): DynamicModule {
    return {
      module: DatabaseModule,
      global: true,
      imports: options.imports || [],
      providers: [
        {
          provide: 'DATABASE_OPTIONS',
          useFactory: options.useFactory,
          inject: options.inject || [],
        },
        DatabaseService,
      ],
      exports: [DatabaseService],
    }
  }
}

// Verwendung in AppModule
@Module({
  imports: [
    DatabaseModule.forRootAsync({
      imports: [ConfigModule],
      useFactory: (config: ConfigService) => ({
        host: config.get('DB_HOST'),
        port: config.get('DB_PORT'),
      }),
      inject: [ConfigService],
    }),
  ],
})
export class AppModule {}
BefehlBeschreibung
@Controller('users')Controller mit Routenpräfix definieren
@Get()GET-Anfrage verarbeiten
@Post()POST-Anfrage verarbeiten
@Put(':id')PUT-Anfrage mit Parameter verarbeiten
@Patch(':id')PATCH-Anfrage verarbeiten
@Delete(':id')DELETE-Anfrage verarbeiten
@All('*')Alle HTTP-Methoden verarbeiten
BefehlBeschreibung
@Param('id') id: stringRoutenparameter extrahieren
@Param('id', ParseIntPipe) id: numberExtrahieren und zu Integer parsen
@Query('page') page: stringQuery-Parameter extrahieren
@Query() query: PaginationDtoAlle Query-Parameter als DTO extrahieren
@Body() dto: CreateUserDtoAnfrage-Body extrahieren und typisieren
@Body('email') email: stringBestimmtes Body-Feld extrahieren
@Headers('authorization') auth: stringAnfrage-Header extrahieren
@Ip() ip: stringClient-IP-Adresse extrahieren
@Req() request: RequestAuf rohes Request-Objekt zugreifen
@Res() response: ResponseAuf rohes Response-Objekt zugreifen
@HttpCode(201)Benutzerdefinierten HTTP-Statuscode setzen
@Header('Cache-Control', 'none')Antwort-Header setzen
@Redirect('/new-url', 301)Weiterleitung als Antwort
import {
  Controller, Get, Post, Put, Delete, Param, Query, Body,
  HttpCode, HttpStatus, ParseIntPipe, UseGuards, UseInterceptors,
} from '@nestjs/common'
import { UsersService } from './users.service'
import { CreateUserDto } from './dto/create-user.dto'
import { UpdateUserDto } from './dto/update-user.dto'
import { JwtAuthGuard } from '../auth/jwt-auth.guard'
import { LoggingInterceptor } from '../common/interceptors/logging.interceptor'

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

  @Post()
  @HttpCode(HttpStatus.CREATED)
  create(@Body() createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto)
  }

  @Get()
  findAll(
    @Query('page', new ParseIntPipe({ optional: true })) page = 1,
    @Query('limit', new ParseIntPipe({ optional: true })) limit = 10,
  ) {
    return this.usersService.findAll(page, limit)
  }

  @Get(':id')
  findOne(@Param('id', ParseIntPipe) id: number) {
    return this.usersService.findOne(id)
  }

  @Put(':id')
  @UseGuards(JwtAuthGuard)
  update(
    @Param('id', ParseIntPipe) id: number,
    @Body() updateUserDto: UpdateUserDto,
  ) {
    return this.usersService.update(id, updateUserDto)
  }

  @Delete(':id')
  @UseGuards(JwtAuthGuard)
  @HttpCode(HttpStatus.NO_CONTENT)
  remove(@Param('id', ParseIntPipe) id: number) {
    return this.usersService.remove(id)
  }
}
BefehlBeschreibung
@Injectable()Klasse als injizierbaren Provider markieren
constructor(private usersService: UsersService)Service über Konstruktor injizieren
@Inject('TOKEN') private configPer benutzerdefiniertem Token injizieren
{ provide: 'TOKEN', useValue: value }Wert-Provider registrieren
{ provide: 'TOKEN', useFactory: () => ... }Factory-Provider registrieren
{ provide: 'TOKEN', useClass: MyClass }Klassen-Provider registrieren
{ provide: 'TOKEN', useExisting: OtherService }Bestehenden Provider als Alias
@Optional() private service?: MyServiceOptionale Dependency Injection
{ provide: 'TOKEN', scope: Scope.REQUEST }Request-bezogener Provider
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { Repository } from 'typeorm'
import { User } from './entities/user.entity'
import { CreateUserDto } from './dto/create-user.dto'
import { UpdateUserDto } from './dto/update-user.dto'

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private readonly usersRepo: Repository<User>,
  ) {}

  async create(dto: CreateUserDto): Promise<User> {
    const exists = await this.usersRepo.findOneBy({ email: dto.email })
    if (exists) {
      throw new ConflictException('Email already registered')
    }
    const user = this.usersRepo.create(dto)
    return this.usersRepo.save(user)
  }

  async findAll(page: number, limit: number) {
    const [items, total] = await this.usersRepo.findAndCount({
      skip: (page - 1) * limit,
      take: limit,
      order: { createdAt: 'DESC' },
    })
    return { items, total, page, limit }
  }

  async findOne(id: number): Promise<User> {
    const user = await this.usersRepo.findOneBy({ id })
    if (!user) throw new NotFoundException(`User #${id} not found`)
    return user
  }

  async update(id: number, dto: UpdateUserDto): Promise<User> {
    const user = await this.findOne(id)
    Object.assign(user, dto)
    return this.usersRepo.save(user)
  }

  async remove(id: number): Promise<void> {
    const result = await this.usersRepo.delete(id)
    if (result.affected === 0) {
      throw new NotFoundException(`User #${id} not found`)
    }
  }
}
npm install class-validator class-transformer
// main.ts - globale Validierung aktivieren
import { ValidationPipe } from '@nestjs/common'

async function bootstrap() {
  const app = await NestFactory.create(AppModule)
  app.useGlobalPipes(new ValidationPipe({
    whitelist: true,           // unbekannte Eigenschaften entfernen
    forbidNonWhitelisted: true, // bei unbekannten Eigenschaften Fehler werfen
    transform: true,           // Payloads automatisch in DTO-Typen transformieren
    transformOptions: {
      enableImplicitConversion: true, // Query-Strings in Zahlen/Booleans konvertieren
    },
  }))
  await app.listen(3000)
}
import {
  IsString, IsEmail, IsOptional, IsInt, Min, Max,
  MinLength, MaxLength, IsEnum, ValidateNested, IsArray,
} from 'class-validator'
import { Type } from 'class-transformer'
import { PartialType, OmitType, PickType, IntersectionType } from '@nestjs/mapped-types'

export class CreateUserDto {
  @IsString()
  @MinLength(2)
  @MaxLength(50)
  name: string

  @IsEmail()
  email: string

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

  @IsOptional()
  @IsEnum(Role)
  role?: Role

  @IsOptional()
  @ValidateNested()
  @Type(() => AddressDto)
  address?: AddressDto
}

// Update-DTO automatisch generieren (alle Felder optional)
export class UpdateUserDto extends PartialType(CreateUserDto) {}

// Bestimmte Felder auswählen
export class LoginDto extends PickType(CreateUserDto, ['email', 'password']) {}

// Bestimmte Felder weglassen
export class PublicUserDto extends OmitType(CreateUserDto, ['password']) {}
BefehlBeschreibung
@UseGuards(AuthGuard)Guard auf Controller oder Route anwenden
@UseGuards(AuthGuard, RolesGuard)Mehrere Guards verketten
app.useGlobalGuards(new AuthGuard())Globalen Guard registrieren
canActivate(context: ExecutionContext)Guard-Logik implementieren
@SetMetadata('roles', ['admin'])Benutzerdefinierte Metadaten für Guards setzen
ReflectorMetadaten in Guards lesen
npm install @nestjs/jwt @nestjs/passport passport passport-jwt
npm install -D @types/passport-jwt
// auth/auth.module.ts
@Module({
  imports: [
    UsersModule,
    JwtModule.registerAsync({
      imports: [ConfigModule],
      useFactory: (config: ConfigService) => ({
        secret: config.get('JWT_SECRET'),
        signOptions: { expiresIn: '1h' },
      }),
      inject: [ConfigService],
    }),
  ],
  providers: [AuthService, JwtStrategy],
  controllers: [AuthController],
  exports: [AuthService],
})
export class AuthModule {}

// auth/jwt.strategy.ts
import { Injectable } from '@nestjs/common'
import { PassportStrategy } from '@nestjs/passport'
import { ExtractJwt, Strategy } from 'passport-jwt'

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(config: ConfigService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: config.get('JWT_SECRET'),
    })
  }

  validate(payload: { sub: number; email: string }) {
    return { id: payload.sub, email: payload.email }
  }
}

// auth/jwt-auth.guard.ts
import { Injectable } from '@nestjs/common'
import { AuthGuard } from '@nestjs/passport'

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

// Verwendung
@Controller('profile')
@UseGuards(JwtAuthGuard)
export class ProfileController {
  @Get()
  getProfile(@Req() req) {
    return req.user // von JwtStrategy.validate() injiziert
  }
}
// roles.decorator.ts
import { SetMetadata } from '@nestjs/common'
export const Roles = (...roles: string[]) => SetMetadata('roles', roles)

// roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'
import { Reflector } from '@nestjs/core'

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

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [
      context.getHandler(),
      context.getClass(),
    ])
    if (!requiredRoles) return true
    const { user } = context.switchToHttp().getRequest()
    return requiredRoles.some(role => user.roles?.includes(role))
  }
}

// Verwendung
@Controller('admin')
@UseGuards(JwtAuthGuard, RolesGuard)
export class AdminController {
  @Get('dashboard')
  @Roles('admin')
  getDashboard() {
    return { message: 'Admin dashboard' }
  }

  @Delete('users/:id')
  @Roles('admin', 'superadmin')
  removeUser(@Param('id', ParseIntPipe) id: number) {
    return this.usersService.remove(id)
  }
}
import { Injectable, NestMiddleware } from '@nestjs/common'
import { Request, Response, NextFunction } from 'express'

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    const start = Date.now()
    res.on('finish', () => {
      const duration = Date.now() - start
      console.log(`${req.method} ${req.path} ${res.statusCode} ${duration}ms`)
    })
    next()
  }
}

// Im Modul registrieren
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes('*')
  }
}
import {
  Injectable, NestInterceptor, ExecutionContext, CallHandler,
} from '@nestjs/common'
import { Observable, map, tap } from 'rxjs'

// Antwortform transformieren
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, { data: T }> {
  intercept(context: ExecutionContext, next: CallHandler): Observable<{ data: T }> {
    return next.handle().pipe(
      map(data => ({ data, timestamp: new Date().toISOString() })),
    )
  }
}

// Ausführungszeit messen
@Injectable()
export class TimingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const start = Date.now()
    return next.handle().pipe(
      tap(() => {
        const duration = Date.now() - start
        const response = context.switchToHttp().getResponse()
        response.setHeader('X-Response-Time', `${duration}ms`)
      }),
    )
  }
}
BefehlBeschreibung
npm install @nestjs/typeorm typeorm pgTypeORM mit PostgreSQL installieren
TypeOrmModule.forRoot({ type: 'postgres', ... })Verbindung in AppModule konfigurieren
TypeOrmModule.forFeature([User])Entity im Feature-Modul registrieren
@InjectRepository(User) private repo: Repository<User>Repository injizieren
npm install @prisma/client
npm install -D prisma
npx prisma init
// prisma/prisma.service.ts
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'
import { PrismaClient } from '@prisma/client'

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
  async onModuleInit() {
    await this.$connect()
  }

  async onModuleDestroy() {
    await this.$disconnect()
  }
}

// prisma/prisma.module.ts
@Global()
@Module({
  providers: [PrismaService],
  exports: [PrismaService],
})
export class PrismaModule {}

// Verwendung in einem Service
@Injectable()
export class UsersService {
  constructor(private prisma: PrismaService) {}

  findAll() {
    return this.prisma.user.findMany()
  }

  findOne(id: number) {
    return this.prisma.user.findUniqueOrThrow({ where: { id } })
  }
}
BefehlBeschreibung
throw new BadRequestException('Invalid input')400 Bad Request
throw new UnauthorizedException('Login required')401 Unauthorized
throw new ForbiddenException('Access denied')403 Forbidden
throw new NotFoundException('User not found')404 Not Found
throw new ConflictException('Email taken')409 Conflict
throw new UnprocessableEntityException('Validation failed')422 Unprocessable
throw new InternalServerErrorException()500 Internal Server Error
import {
  ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus,
} from '@nestjs/common'
import { Request, Response } from 'express'

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp()
    const response = ctx.getResponse<Response>()
    const request = ctx.getRequest<Request>()

    const status = exception instanceof HttpException
      ? exception.getStatus()
      : HttpStatus.INTERNAL_SERVER_ERROR

    const message = exception instanceof HttpException
      ? exception.getResponse()
      : 'Internal server error'

    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      message: typeof message === 'string' ? message : (message as any).message,
    })
  }
}

// Global in main.ts registrieren
app.useGlobalFilters(new AllExceptionsFilter())
npm install @nestjs/config
// app.module.ts
import { ConfigModule, ConfigService } from '@nestjs/config'

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: ['.env.local', '.env'],
      validationSchema: Joi.object({
        NODE_ENV: Joi.string().valid('development', 'production', 'test').default('development'),
        PORT: Joi.number().default(3000),
        DATABASE_URL: Joi.string().required(),
        JWT_SECRET: Joi.string().required(),
      }),
    }),
  ],
})
export class AppModule {}

// Verwendung in jedem Service
@Injectable()
export class AppService {
  constructor(private config: ConfigService) {}

  getPort(): number {
    return this.config.get<number>('PORT', 3000)
  }
}
import { Test, TestingModule } from '@nestjs/testing'
import { UsersService } from './users.service'
import { getRepositoryToken } from '@nestjs/typeorm'
import { User } from './entities/user.entity'

describe('UsersService', () => {
  let service: UsersService

  const mockRepository = {
    create: jest.fn(),
    save: jest.fn(),
    findOneBy: jest.fn(),
    findAndCount: jest.fn(),
    delete: jest.fn(),
  }

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UsersService,
        { provide: getRepositoryToken(User), useValue: mockRepository },
      ],
    }).compile()

    service = module.get<UsersService>(UsersService)
  })

  it('should create a user', async () => {
    const dto = { name: 'Alice', email: 'alice@example.com', password: 'secret123' }
    mockRepository.findOneBy.mockResolvedValue(null)
    mockRepository.create.mockReturnValue(dto)
    mockRepository.save.mockResolvedValue({ id: 1, ...dto })

    const result = await service.create(dto)
    expect(result.id).toBe(1)
    expect(mockRepository.save).toHaveBeenCalled()
  })
})
import { Test, TestingModule } from '@nestjs/testing'
import { INestApplication, ValidationPipe } from '@nestjs/common'
import * as request from 'supertest'
import { AppModule } from '../src/app.module'

describe('UsersController (e2e)', () => {
  let app: INestApplication

  beforeAll(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    }).compile()

    app = moduleFixture.createNestApplication()
    app.useGlobalPipes(new ValidationPipe({ whitelist: true }))
    await app.init()
  })

  afterAll(async () => {
    await app.close()
  })

  it('/users (POST) creates a user', () => {
    return request(app.getHttpServer())
      .post('/users')
      .send({ name: 'Alice', email: 'alice@example.com', password: 'secret123' })
      .expect(201)
      .expect((res) => {
        expect(res.body.name).toBe('Alice')
        expect(res.body.id).toBeDefined()
      })
  })

  it('/users (GET) returns paginated list', () => {
    return request(app.getHttpServer())
      .get('/users?page=1&limit=10')
      .expect(200)
      .expect((res) => {
        expect(res.body.items).toBeInstanceOf(Array)
        expect(res.body.total).toBeDefined()
      })
  })
})
  1. Den Resource-Generator verwendennest g resource erstellt das gesamte CRUD-Gerüst (Modul, Controller, Service, DTOs, Entity, Test) in Sekunden und hält das Projekt konsistent.

  2. Globale ValidationPipe mit Whitelist aktivierenwhitelist: true und forbidNonWhitelisted: true setzen, um unerwartete Felder aus allen Anfragen automatisch zu entfernen oder abzulehnen.

  3. Mapped Types für DTOs verwendenPartialType, PickType, OmitType und IntersectionType halten DTOs DRY, indem Update-/Query-DTOs vom Create-DTO abgeleitet werden.

  4. Konstruktor-Injection bevorzugen — NestJS die Dependency Injection über Konstruktoren handhaben lassen, statt manuelle @Inject()-Tokens zu verwenden, es sei denn, benutzerdefinierte Provider werden benötigt.

  5. Controller schlank halten — Controller sollten nur Eingaben parsen und Ausgaben zurückgeben. Alle Geschäftslogik in Services verschieben für bessere Testbarkeit und Wiederverwendung.

  6. ConfigModule für allen Umgebungszugriff verwenden — Niemals process.env direkt in Services lesen. ConfigService mit Validierungsschemas verwenden, damit fehlende Variablen beim Start schnell fehlschlagen.

  7. Guards und Interceptors angemessen platzieren — Sie auf Controller- oder Routen-Ebene mit Dekoratoren anwenden, statt global, es sei denn, sie gelten wirklich überall.

  8. Unit- und E2E-Tests schreiben — Services mit gemockten Abhängigkeiten über Test.createTestingModule() testen, und HTTP-Endpunkte mit supertest als E2E-Tests prüfen.

  9. Prisma oder TypeORM konsistent verwenden — Ein ORM wählen und projektübergreifend verwenden. Prisma bietet bessere Typsicherheit; TypeORM bietet mehr Flexibilität mit Dekoratoren.

  10. Nach Feature strukturieren, nicht nach Typ — Dateien nach Domain gruppieren (users/, auth/, orders/) statt nach Typ (controllers/, services/). Das hält zusammengehörigen Code zusammen und Module eigenständig.

  11. Fehler mit Ausnahmen behandeln — Eingebaute HTTP-Ausnahmen (NotFoundException, ConflictException) aus Services werfen. Exception-Filter für benutzerdefinierte Fehlerformatierung verwenden.

  12. async/await konsistent verwenden — NestJS verarbeitet Promises nativ. Async-Werte aus Service-Methoden zurückgeben und das Framework die Antwort serialisieren lassen.