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 |
@Module 的 providers 数组 |
@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进阶