NestJS入门指南:Java开发者的Spring Boot体验

NestJS入门指南:Java开发者的Spring Boot体验

预计时间 :1-2 周(本章是重点


🎯 本章目标

NestJS 是 Java 开发者进入 Node.js 世界的最佳框架,它的架构深受 Spring Boot 影响,你会感到非常亲切。


1. NestJS 与 Spring Boot 对照

Spring Boot NestJS 说明
@SpringBootApplication @Module 应用入口
@RestController @Controller 控制器
@Service @Injectable 服务层
@Autowired 构造函数注入 依赖注入
@GetMapping @Get() GET 路由
@PostMapping @Post() POST 路由
@PathVariable @Param('id') 路径参数
@RequestBody @Body() 请求体
@RequestParam @Query('name') 查询参数
@RequestHeader @Headers('token') 请求头
Filter / Interceptor Guard / Interceptor 拦截层
application.yml ConfigModule 配置
Spring Data JPA Prisma / TypeORM ORM
@Valid DTO class-validator DTO 数据验证
Swagger / SpringDoc @nestjs/swagger API 文档

2. 创建项目

bash 复制代码
# 安装 NestJS CLI
pnpm add -g @nestjs/cli

# 创建项目
nest new my-api

# 目录结构
my-api/
├── src/
│   ├── app.controller.ts   # 控制器
│   ├── app.service.ts      # 服务
│   ├── app.module.ts       # 模块
│   └── main.ts             # 入口
├── test/
├── nest-cli.json
├── package.json
└── tsconfig.json

3. 模块(Module)

typescript 复制代码
// app.module.ts --- 类似 Spring 的 @Configuration
import { Module } from '@nestjs/common';
import { UserController } from './user/user.controller';
import { UserService } from './user/user.service';

@Module({
  imports: [],         // 导入其他模块
  controllers: [UserController], // 注册控制器
  providers: [UserService],      // 注册服务(可注入)
  exports: [UserService],        // 对外暴露的服务
})
export class AppModule {}

模块拆分

bash 复制代码
# 用 CLI 生成模块
nest g module users     # 生成 users 模块
nest g module auth
nest g module products
typescript 复制代码
// users/users.module.ts
@Module({
  controllers: [UsersController],
  providers: [UsersService, UsersRepository],
  exports: [UsersService], // 其他模块可以注入 UsersService
})
export class UsersModule {}

// app.module.ts
@Module({
  imports: [UsersModule, AuthModule, ProductsModule],
})
export class AppModule {}

4. 控制器(Controller)

typescript 复制代码
import {
  Controller, Get, Post, Put, Delete,
  Param, Body, Query, HttpCode, HttpStatus
} from '@nestjs/common';

@Controller('users') // 路由前缀 /users
export class UsersController {

  // 依赖注入(构造函数注入,类似 Spring)
  constructor(private readonly usersService: UsersService) {}

  @Get()          // GET /users
  async findAll(
    @Query('page') page: number = 1,
    @Query('limit') limit: number = 10
  ) {
    return this.usersService.findAll(page, limit);
  }

  @Get(':id')     // GET /users/:id
  async findOne(@Param('id') id: string) {
    return this.usersService.findOne(id);
  }

  @Post()         // POST /users
  @HttpCode(HttpStatus.CREATED) // 返回 201
  async create(@Body() createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto);
  }

  @Put(':id')     // PUT /users/:id
  async update(
    @Param('id') id: string,
    @Body() updateUserDto: UpdateUserDto
  ) {
    return this.usersService.update(id, updateUserDto);
  }

  @Delete(':id')  // DELETE /users/:id
  @HttpCode(HttpStatus.NO_CONTENT) // 返回 204
  async remove(@Param('id') id: string) {
    await this.usersService.remove(id);
  }
}

5. 服务(Service / Provider)

typescript 复制代码
import { Injectable, NotFoundException } from '@nestjs/common';

@Injectable() // 标记为可注入,类似 Spring 的 @Service
export class UsersService {

  // 可以注入其他服务
  constructor(
    private readonly userRepository: UsersRepository,
    private readonly emailService: EmailService,
  ) {}

  async findAll(page: number, limit: number) {
    return this.userRepository.findAll({ skip: (page - 1) * limit, take: limit });
  }

  async findOne(id: string) {
    const user = await this.userRepository.findById(id);
    if (!user) {
      throw new NotFoundException(`User ${id} not found`); // 自动返回 404
    }
    return user;
  }

  async create(dto: CreateUserDto) {
    const user = await this.userRepository.create(dto);
    await this.emailService.sendWelcome(user.email);
    return user;
  }

  async update(id: string, dto: UpdateUserDto) {
    await this.findOne(id); // 确保存在
    return this.userRepository.update(id, dto);
  }

  async remove(id: string) {
    await this.findOne(id);
    await this.userRepository.delete(id);
  }
}

6. DTO 与数据验证

bash 复制代码
pnpm add class-validator class-transformer
typescript 复制代码
// users/dto/create-user.dto.ts
import { IsString, IsEmail, MinLength, IsOptional, IsNumber } from 'class-validator';

export class CreateUserDto {
  @IsString()
  @MinLength(2)
  name!: string;

  @IsEmail()
  email!: string;

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

  @IsOptional()
  @IsNumber()
  age?: number;
}

// users/dto/update-user.dto.ts
import { PartialType } from '@nestjs/mapped-types';

// 所有属性变为可选
export class UpdateUserDto extends PartialType(CreateUserDto) {}

启用全局验证管道

typescript 复制代码
// main.ts
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.useGlobalPipes(new ValidationPipe({
    whitelist: true,       // 自动过滤未定义的属性
    forbidNonWhitelisted: true, // 有未定义属性时报错
    transform: true,       // 自动类型转换
  }));

  await app.listen(3000);
}

7. 异常处理

typescript 复制代码
import {
  HttpException, HttpStatus,
  NotFoundException, BadRequestException,
  UnauthorizedException, ForbiddenException,
  ConflictException
} from '@nestjs/common';

// 内置异常(自动返回对应 HTTP 状态码)
throw new NotFoundException('User not found');      // 404
throw new BadRequestException('Invalid email');     // 400
throw new UnauthorizedException('Token expired');   // 401
throw new ForbiddenException('No permission');      // 403
throw new ConflictException('Email already exists'); // 409

// 自定义异常
throw new HttpException(
  { status: HttpStatus.IM_A_TEAPOT, message: "I'm a teapot" },
  HttpStatus.IM_A_TEAPOT
);

// 全局异常过滤器
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';

@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();

    const status = exception instanceof HttpException
      ? exception.getStatus()
      : 500;

    response.status(status).json({
      statusCode: status,
      message: exception instanceof HttpException
        ? exception.message
        : 'Internal server error',
      timestamp: new Date().toISOString(),
    });
  }
}

// 注册
app.useGlobalFilters(new GlobalExceptionFilter());

8. 依赖注入(DI)

typescript 复制代码
// 三种注入方式

// 1. 构造函数注入(推荐,类似 Spring)
@Injectable()
class UsersService {
  constructor(
    private readonly repo: UsersRepository,
    private readonly logger: LoggerService,
  ) {}
}

// 2. 自定义 Token
const CONFIG_TOKEN = 'CONFIG';

@Module({
  providers: [
    {
      provide: CONFIG_TOKEN,
      useValue: { port: 3000, dbUrl: '...' }
    },
    UsersService,
  ]
})
class AppModule {}

// 使用
@Injectable()
class UsersService {
  constructor(@Inject(CONFIG_TOKEN) private config: Config) {}
}

// 3. 工厂提供者
@Module({
  providers: [
    {
      provide: 'DATABASE_CONNECTION',
      useFactory: async (config: ConfigService) => {
        return await createConnection(config.get('DATABASE_URL'));
      },
      inject: [ConfigService],
    }
  ]
})

9. NestJS 启动流程详解

css 复制代码
main.ts 执行
    │
    ▼
NestFactory.create(AppModule)  ← 启动 IoC 容器
    │
    ├─→ 解析 @Module 装饰器,扫描所有 imports
    ├─→ 扫描 controllers,注册路由
    ├─→ 扫描 providers,解析依赖关系
    ├─→ 实例化 providers(单例默认)
    ├─→ 注入依赖到 controllers 和 providers
    │
    ▼
app.useGlobalPipes(...)  ← 注册全局管道
app.useGlobalFilters(...) ← 注册全局过滤器
app.useGlobalGuards(...)  ← 注册全局守卫
    │
    ▼
app.listen(3000)  ← 启动 HTTP 服务器

与 Spring Boot 启动对比

Spring Boot NestJS
SpringApplication.run() NestFactory.create()
@ComponentScan @Moduleproviders 数组
@Bean 方法 useFactory 提供者
@ConditionalOnProperty 动态模块
ApplicationRunner OnModuleInit 钩子

10. 生命周期钩子

typescript 复制代码
import {
  Injectable, OnModuleInit, OnModuleDestroy,
  OnApplicationBootstrap, OnApplicationShutdown
} from '@nestjs/common';

@Injectable()
export class AppService implements OnModuleInit, OnModuleDestroy {

  // 模块初始化后执行(类似 Spring 的 @PostConstruct)
  async onModuleInit() {
    console.log('模块已初始化,可以连接数据库了');
    await this.connectToDatabase();
  }

  // 应用关闭前执行(类似 Spring 的 @PreDestroy)
  async onModuleDestroy() {
    console.log('模块即将销毁,清理资源');
    await this.closeConnections();
  }
}

// 执行顺序:
// 1. onModuleInit         所有模块初始化完成
// 2. onApplicationBootstrap 应用已可以接收请求
// 3. onModuleDestroy      应用收到关闭信号
// 4. onApplicationShutdown 应用即将关闭

11. 完整的模块示例:Users

typescript 复制代码
// users/entities/user.entity.ts
export class User {
  id!: number;
  email!: string;
  name!: string;
  password!: string;
  role!: 'USER' | 'ADMIN';
  createdAt!: Date;
}

// users/dto/create-user.dto.ts
import { IsEmail, IsString, MinLength, IsOptional } from 'class-validator';

export class CreateUserDto {
  @IsEmail({}, { message: '邮箱格式不正确' })
  email!: string;

  @IsString()
  @MinLength(2, { message: '用户名至少 2 个字符' })
  name!: string;

  @IsString()
  @MinLength(8, { message: '密码至少 8 位' })
  password!: string;
}

// users/users.controller.ts
import { Controller, Get, Post, Body, Param, Query, Delete, HttpCode } from '@nestjs/common';

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

  @Post()
  @HttpCode(201)
  async create(@Body() dto: CreateUserDto) {
    return this.usersService.create(dto);
  }

  @Get()
  async findAll(
    @Query('page') page: string = '1',
    @Query('limit') limit: string = '10',
    @Query('search') search?: string
  ) {
    return this.usersService.findAll({
      page: parseInt(page),
      limit: parseInt(limit),
      search,
    });
  }

  @Get(':id')
  async findOne(@Param('id') id: string) {
    return this.usersService.findOne(parseInt(id));
  }

  @Delete(':id')
  @HttpCode(204)
  async remove(@Param('id') id: string) {
    await this.usersService.remove(parseInt(id));
  }
}

// users/users.service.ts
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';

@Injectable()
export class UsersService {
  private users: User[] = []; // 内存存储,实际项目用 Prisma

  async create(dto: CreateUserDto): Promise<User> {
    const exists = this.users.find(u => u.email === dto.email);
    if (exists) throw new ConflictException('Email already in use');

    const user: User = {
      id: Date.now(),
      ...dto,
      role: 'USER',
      createdAt: new Date()
    };
    this.users.push(user);
    return user;
  }

  async findAll(params: { page: number; limit: number; search?: string }) {
    let result = this.users;
    if (params.search) {
      result = result.filter(u =>
        u.name.toLowerCase().includes(params.search!.toLowerCase())
      );
    }
    const start = (params.page - 1) * params.limit;
    return {
      items: result.slice(start, start + params.limit),
      total: result.length,
      page: params.page,
    };
  }

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

  async remove(id: number): Promise<void> {
    const index = this.users.findIndex(u => u.id === id);
    if (index === -1) throw new NotFoundException(`User #${id} not found`);
    this.users.splice(index, 1);
  }
}

// users/users.module.ts
@Module({
  controllers: [UsersController],
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}

12. main.ts 完整示例

typescript 复制代码
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // 全局前缀
  app.setGlobalPrefix('api'); // 所有路由加上 /api 前缀

  // CORS 配置
  app.enableCors({
    origin: ['http://localhost:3000', 'http://localhost:5173'],
    credentials: true,
  });

  // 全局验证管道
  app.useGlobalPipes(new ValidationPipe({
    whitelist: true,            // 过滤未定义的属性
    forbidNonWhitelisted: true, // 有未定义属性时报错
    transform: true,            // 自动类型转换(string → number)
  }));

  // Swagger 文档
  const config = new DocumentBuilder()
    .setTitle('My API')
    .setVersion('1.0')
    .addBearerAuth()
    .build();
  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('docs', app, document); // 访问 /docs

  // 启动
  const port = process.env.PORT || 3000;
  await app.listen(port);
  console.log(`🚀 Server running on http://localhost:${port}`);
  console.log(`📖 Docs at http://localhost:${port}/docs`);
}

bootstrap();

下一步:NestJS进阶

相关推荐
糖拌西瓜皮1 小时前
Express框架快速上手:中间件、路由与错误处理
javascript·node.js
半个落月4 小时前
从递归到快速排序:用 JavaScript 把分治思想讲明白
javascript·算法·面试
半个落月4 小时前
从 Tokenization 到 Embedding:用 Node.js 搞懂大模型为什么先“分词”再“向量化”
人工智能·node.js
小兔崽子去哪了4 小时前
Vue3 + Pinia 集成 IGV.js 实现 BAM 文件在线浏览
javascript·vue.js·后端
小月土星5 小时前
JavaScript 快速排序:从 pivot、双指针到分治思想
javascript·算法·面试
小月土星5 小时前
JavaScript 递归入门:从 1 到 n 求和,再到数组扁平化
javascript·算法·面试
还有多久拿退休金5 小时前
一个 var 让整个团队加班到凌晨——JS 闭包的那些暗坑
前端·javascript
weedsfly5 小时前
用了 React/Vue 之后,这些 DOM 操作的坑你踩过几个?
前端·javascript
Asize5 小时前
Ajax 入门:从 JSON 序列化到 XMLHttpRequest
前端·javascript·前端框架