辰風依恛
文章35
标签0
分类11
NestJS项目从零到一

NestJS项目从零到一

NestJS项目从零到一

目录

  1. 项目初始化与脚手架
  2. 项目结构规划
  3. 数据库连接配置
  4. DTO数据验证与封装
  5. 统一返回结果封装
  6. JWT认证与授权
  7. 接口文档生成
  8. CRUD接口实现示例
  9. 文件上传功能
  10. 大文件分片上传
  11. 路由守卫与拦截器
  12. 日志系统
  13. 环境配置管理
  14. 部署与生产环境

1. 项目初始化与脚手架

1.1 安装NestJS CLI

1
npm install -g @nestjs/cli

1.2 创建新项目

使用NestJS CLI创建项目:

1
2
nest new nest-serve
cd nest-serve

项目创建选项:

创建项目时会询问包管理器选择:

1
2
3
4
? Which package manager would you ❤️ to use? 
❯ npm
yarn
pnpm

项目结构预览:

创建完成后会自动生成标准项目结构:

1
2
3
4
5
6
7
8
9
10
11
12
nest-serve/
├── src/
│ ├── main.ts # 应用入口
│ ├── app.module.ts # 根模块
│ ├── app.controller.ts # 根控制器
│ └── app.service.ts # 根服务
├── test/ # 测试文件
├── .eslintrc.js # ESLint配置
├── .prettierrc # Prettier配置
├── tsconfig.json # TypeScript配置
├── package.json # 项目依赖
└── README.md # 项目说明

验证项目创建成功:

1
2
3
4
# 启动开发服务器
npm run start:dev

# 访问 http://localhost:3000 查看欢迎页面

1.3 项目结构说明

1
2
3
4
5
6
7
8
9
nest-serve/
├── src/
│ ├── main.ts # 应用入口文件
│ ├── app.module.ts # 根模块
│ ├── app.controller.ts # 根控制器
│ └── app.service.ts # 根服务
├── test/ # 测试文件目录
├── package.json
└── tsconfig.json

1.4 移除测试模块(可选)

如果不需要测试功能,可以删除test目录和相关的测试依赖:

1
rm -rf test/

在package.json中移除测试相关依赖:

1
2
3
4
5
6
7
8
9
10
{
"devDependencies": {
// 移除以下测试相关依赖
// "@nestjs/testing": "^11.0.1",
// "@types/jest": "^29.5.14",
// "jest": "^29.7.0",
// "supertest": "^7.0.0",
// "ts-jest": "^29.2.5"
}
}

2. 项目结构规划

2.1 推荐的模块化结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
src/
├── common/ # 公共模块
│ ├── decorator/ # 装饰器
│ ├── dto/ # 数据传输对象
│ ├── entities/ # 实体类
│ ├── filter/ # 异常过滤器
│ ├── guards/ # 守卫
│ ├── interceptor/ # 拦截器
│ ├── pipeTransform/ # 管道
│ ├── service/ # 公共服务
│ └── utils/ # 工具函数
├── config/ # 配置文件
├── module/ # 业务模块
│ ├── auth/ # 认证模块
│ ├── users/ # 用户模块
│ ├── upload/ # 文件上传模块
│ └── ... # 其他业务模块
└── types/ # TypeScript类型定义

2.2 创建模块结构

1
2
3
4
5
# 创建目录结构
mkdir -p src/common/{decorator,dto,entities,filter,guards,interceptor,pipeTransform,service,utils}
mkdir -p src/config
mkdir -p src/module/{auth,users,upload}
mkdir -p src/types

3. 数据库连接配置

3.1 安装数据库依赖

1
2
npm install @nestjs/typeorm typeorm mysql2 sqlite3
npm install @nestjs/config

3.2 配置多数据库连接

创建数据库配置文件 src/config/db.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import { join } from "path";

// 获取环境变量
const env = process.env;

/** 数据库配置类型 */
export interface MysqlConfig {
DB_TYPE: string;
DB_HOST: string;
DB_PORT: number;
DB_USER: string;
DB_PASSWORD: string;
DB_DATABASE: string;
}

/** Sqlite配置类型 */
export interface SqliteConfig {
DB_TYPE: string;
DB_DATABASE: string;
}

// MySQL配置
const mysqlConfig: MysqlConfig = {
DB_TYPE: "mysql",
DB_HOST: env.DB_HOST || "127.0.0.1",
DB_PORT: Number(env.DB_PORT) || 3306,
DB_USER: env.DB_USER || "root",
DB_PASSWORD: env.DB_PASSWORD || "123456",
DB_DATABASE: env.DB_DATABASE || "nest-serve",
};

// SQLite配置
const sqliteConfig: SqliteConfig = {
DB_TYPE: "sqlite",
DB_DATABASE: join(process.cwd(), env.DB_DATABASE || "sqlitedata/nest.db"),
};

// 根据环境变量选择数据库配置
const DB_TYPE = env.DB_TYPE;
const dbConfig = DB_TYPE === "sqlite" ? sqliteConfig : mysqlConfig;

export default { dbConfig };

3.3 在主模块中配置数据库

src/app.module.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import { Module } from "@nestjs/common";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { TypeOrmModule } from "@nestjs/typeorm";
import { join } from "path";
import DBConfig from "./config/db";

@Module({
imports: [
// 配置模块
ConfigModule.forRoot({
isGlobal: true,
envFilePath: `.env.${process.env.NODE_ENV || "dev"}`,
}),

// 数据库连接
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => {
const dbType = DBConfig.dbConfig.DB_TYPE as "mysql" | "sqlite";

if (dbType === "sqlite") {
return {
type: dbType,
database: DBConfig.dbConfig.DB_DATABASE,
synchronize: process.env.NODE_ENV !== "production",
logging: process.env.NODE_ENV !== "production",
entities: [join(__dirname, "module/**/*.entity.{ts,js}")],
};
} else {
return {
type: dbType,
host: DBConfig.dbConfig.DB_HOST,
port: DBConfig.dbConfig.DB_PORT,
username: DBConfig.dbConfig.DB_USER,
password: DBConfig.dbConfig.DB_PASSWORD,
database: DBConfig.dbConfig.DB_DATABASE,
synchronize: process.env.NODE_ENV !== "production",
logging: process.env.NODE_ENV !== "production",
entities: [join(__dirname, "module/**/*.entity.{ts,js}")],
};
}
},
inject: [ConfigService],
}),
],
controllers: [],
providers: [],
})
export class AppModule {}

3.4 环境配置文件

创建 .env.dev

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 数据库配置
DB_TYPE=mysql
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWORD=123456
DB_DATABASE=nest-serve

# JWT配置
JWT_SECRET=your-secret-key
JWT_EXPIRES_IN=7d

# 应用配置
PORT=3000
NODE_ENV=dev

4. DTO数据验证与封装

4.1 安装验证依赖

1
npm install class-validator class-transformer

4.2 创建基础DTO

src/common/dto/base.dto.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { IsOptional, IsNumber, Min } from 'class-validator';
import { Type } from 'class-transformer';

export class PaginationDto {
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
page?: number = 1;

@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
pageSize?: number = 10;
}

export class IdDto {
@IsNumber()
@Min(1)
id: number;
}

4.3 用户模块DTO示例

src/module/users/dto/create-user.dto.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { IsString, IsEmail, IsNotEmpty, MinLength, MaxLength } from 'class-validator';

export class CreateUserDto {
@IsString()
@IsNotEmpty({ message: '用户名不能为空' })
@MinLength(2, { message: '用户名至少2个字符' })
@MaxLength(20, { message: '用户名最多20个字符' })
username: string;

@IsEmail({}, { message: '邮箱格式不正确' })
@IsNotEmpty({ message: '邮箱不能为空' })
email: string;

@IsString()
@IsNotEmpty({ message: '密码不能为空' })
@MinLength(6, { message: '密码至少6个字符' })
password: string;
}

src/module/users/dto/update-user.dto.ts

1
2
3
4
import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';

export class UpdateUserDto extends PartialType(CreateUserDto) {}

5. 统一返回结果封装

5.1 创建响应接口

src/common/types/public.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
export interface ResponseData<T = any> {
code: number;
message: string;
data?: T;
success: boolean;
}

export interface PaginationData<T = any> {
list: T[];
total: number;
page: number;
pageSize: number;
totalPages: number;
}

export class ResponseResult {
static success<T>(data?: T, message = '操作成功'): ResponseData<T> {
return {
code: 200,
message,
data,
success: true,
};
}

static pagination<T>(list: T[], total: number, page: number, pageSize: number): ResponseData<PaginationData<T>> {
return this.success({
list,
total,
page,
pageSize,
totalPages: Math.ceil(total / pageSize),
});
}

static error(message = '操作失败', code = 500): ResponseData {
return {
code,
message,
success: false,
};
}
}

5.2 创建统一响应拦截器

src/common/interceptor/response.interceptor.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { ResponseData } from '../types/public';

@Injectable()
export class ResponseInterceptor<T> implements NestInterceptor<T, ResponseData<T>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<ResponseData<T>> {
return next.handle().pipe(
map((data) => {
// 如果已经是统一格式,直接返回
if (data && typeof data === 'object' && 'code' in data && 'success' in data) {
return data;
}

// 统一格式化响应
return {
code: 200,
message: '操作成功',
data: data || null,
success: true,
};
}),
);
}
}

6. JWT认证与授权

6.1 安装JWT依赖

1
2
npm install @nestjs/jwt bcrypt
npm install @types/bcrypt -D

6.2 创建JWT配置

src/config/jwt.ts

1
2
3
4
5
6
import { registerAs } from '@nestjs/config';

export default registerAs('jwt', () => ({
secret: process.env.JWT_SECRET || 'your-secret-key',
expiresIn: process.env.JWT_EXPIRES_IN || '7d',
}));

6.3 创建认证守卫

src/common/guards/auth.guard.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';

@Injectable()
export class AuthGuard implements CanActivate {
constructor(private jwtService: JwtService) {}

async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);

if (!token) {
throw new UnauthorizedException('请先登录');
}

try {
const payload = await this.jwtService.verifyAsync(token);
request.user = payload;
} catch {
throw new UnauthorizedException('令牌无效或已过期');
}

return true;
}

private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}

6.4 创建角色守卫

src/common/guards/roles.guard.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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.get<string[]>('roles', context.getHandler());

if (!requiredRoles) {
return true;
}

const { user } = context.switchToHttp().getRequest();
return requiredRoles.some((role) => user.roles?.includes(role));
}
}

6.5 创建角色装饰器

src/common/decorator/roles.decorator.ts

1
2
3
4
import { SetMetadata } from '@nestjs/common';

export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);

7. 接口文档生成

7.1 安装Swagger依赖

1
npm install @nestjs/swagger swagger-ui-express

7.2 配置Swagger

src/main.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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.useGlobalPipes(new ValidationPipe());

// Swagger配置
const config = new DocumentBuilder()
.setTitle('NestJS API文档')
.setDescription('NestJS项目API接口文档')
.setVersion('1.0')
.addBearerAuth()
.build();

const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);

await app.listen(process.env.PORT || 3000);
}
bootstrap();

7.3 在控制器中使用Swagger装饰器

src/module/users/users.controller.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { AuthGuard } from '../../common/guards/auth.guard';

@ApiTags('用户管理')
@Controller('users')
@UseGuards(AuthGuard)
@ApiBearerAuth()
export class UsersController {
constructor(private readonly usersService: UsersService) {}

@Post()
@ApiOperation({ summary: '创建用户' })
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}

@Get()
@ApiOperation({ summary: '获取用户列表' })
findAll() {
return this.usersService.findAll();
}

@Get(':id')
@ApiOperation({ summary: '获取用户详情' })
findOne(@Param('id') id: string) {
return this.usersService.findOne(+id);
}

@Patch(':id')
@ApiOperation({ summary: '更新用户信息' })
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
return this.usersService.update(+id, updateUserDto);
}

@Delete(':id')
@ApiOperation({ summary: '删除用户' })
remove(@Param('id') id: string) {
return this.usersService.remove(+id);
}
}

8. CRUD接口实现示例

8.1 创建用户实体

src/module/users/entities/user.entity.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';

@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;

@Column({ unique: true })
username: string;

@Column({ unique: true })
email: string;

@Column()
password: string;

@Column({ default: 'user' })
role: string;

@Column({ default: true })
isActive: boolean;

@CreateDateColumn()
createdAt: Date;

@UpdateDateColumn()
updatedAt: Date;
}

8.2 创建用户服务

src/module/users/users.service.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import * as bcrypt from 'bcrypt';
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 usersRepository: Repository<User>,
) {}

async create(createUserDto: CreateUserDto): Promise<User> {
// 密码加密
const hashedPassword = await bcrypt.hash(createUserDto.password, 10);

const user = this.usersRepository.create({
...createUserDto,
password: hashedPassword,
});

return this.usersRepository.save(user);
}

async findAll(): Promise<User[]> {
return this.usersRepository.find();
}

async findOne(id: number): Promise<User> {
const user = await this.usersRepository.findOne({ where: { id } });
if (!user) {
throw new NotFoundException(`用户 #${id} 不存在`);
}
return user;
}

async update(id: number, updateUserDto: UpdateUserDto): Promise<User> {
const user = await this.findOne(id);

// 如果更新密码,需要重新加密
if (updateUserDto.password) {
updateUserDto.password = await bcrypt.hash(updateUserDto.password, 10);
}

Object.assign(user, updateUserDto);
return this.usersRepository.save(user);
}

async remove(id: number): Promise<void> {
const user = await this.findOne(id);
await this.usersRepository.remove(user);
}

async findByUsername(username: string): Promise<User | undefined> {
return this.usersRepository.findOne({ where: { username } });
}
}

8.3 使用NestJS CLI快速生成CRUD模块

NestJS CLI提供了强大的资源生成命令,可以快速创建完整的CRUD模块:

1
2
# 生成完整的用户模块(包含控制器、服务、实体、DTO等)
nest g resource module/users --no-spec

这个命令会自动生成以下文件:

1
2
3
4
5
6
7
8
9
src/module/users/
├── users.controller.ts # 控制器
├── users.service.ts # 服务
├── entities/
│ └── user.entity.ts # 实体类
├── dto/
│ ├── create-user.dto.ts # 创建DTO
│ └── update-user.dto.ts # 更新DTO
└── users.module.ts # 模块文件

命令参数说明:

  • resource: 生成完整的资源模块
  • module/users: 指定模块路径和名称
  • --no-spec: 不生成测试文件(如果不需要测试)

生成的文件结构:

src/module/users/users.module.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { User } from './entities/user.entity';

@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}

其他有用的CLI命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 单独生成控制器
nest g controller module/users/users --no-spec

# 单独生成服务
nest g service module/users/users --no-spec

# 生成模块
nest g module module/users

# 生成实体类
nest g class module/users/entities/user.entity --no-spec

# 生成DTO
nest g class module/users/dto/create-user.dto --no-spec

优势:

  1. 快速开发:一键生成完整模块结构
  2. 标准化:遵循NestJS最佳实践
  3. 一致性:所有模块结构统一
  4. 可维护性:便于团队协作和代码维护

注意事项:

  • 生成后需要根据实际需求调整实体字段
  • 可能需要添加额外的验证规则
  • 根据业务逻辑完善服务层方法

9. 文件上传功能

9.1 安装文件上传依赖

1
npm install multer @types/multer

9.2 创建文件上传配置

src/config/multer.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import { diskStorage } from 'multer';
import { extname } from 'path';
import { v4 as uuidv4 } from 'uuid';

export const multerConfig = {
storage: diskStorage({
destination: './uploads',
filename: (req, file, callback) => {
const uniqueName = `${uuidv4()}${extname(file.originalname)}`;
callback(null, uniqueName);
},
}),

fileFilter: (req, file, callback) => {
const allowedMimes = [
'image/jpeg',
'image/png',
'image/gif',
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
];

if (allowedMimes.includes(file.mimetype)) {
callback(null, true);
} else {
callback(new Error('不支持的文件类型'), false);
}
},

limits: {
fileSize: 10 * 1024 * 1024, // 10MB
},
};

9.3 创建文件上传模块

src/module/upload/upload.controller.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import { Controller, Post, UseInterceptors, UploadedFile, HttpException, HttpStatus } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { ApiTags, ApiOperation, ApiConsumes, ApiBody } from '@nestjs/swagger';
import { multerConfig } from '../../config/multer';

@ApiTags('文件上传')
@Controller('upload')
export class UploadController {
@Post('file')
@ApiOperation({ summary: '单文件上传' })
@ApiConsumes('multipart/form-data')
@ApiBody({
schema: {
type: 'object',
properties: {
file: {
type: 'string',
format: 'binary',
},
},
},
})
@UseInterceptors(FileInterceptor('file', multerConfig))
uploadFile(@UploadedFile() file: Express.Multer.File) {
if (!file) {
throw new HttpException('文件上传失败', HttpStatus.BAD_REQUEST);
}

return {
filename: file.filename,
originalname: file.originalname,
size: file.size,
mimetype: file.mimetype,
path: `/uploads/${file.filename}`,
};
}
}

10. 大文件分片上传

10.1 安装大文件上传依赖

1
npm install tus-node-server

10.2 创建大文件上传服务

src/module/upload/large-file.service.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { Injectable } from '@nestjs/common';
import { Server } from 'tus-node-server';
import { join } from 'path';

@Injectable()
export class LargeFileService {
private tusServer: Server;

constructor() {
this.tusServer = new Server({
path: '/files',
datastore: new tus.FileSystemStore({
directory: join(process.cwd(), 'uploads/large-files'),
}),
});
}

getServer(): Server {
return this.tusServer;
}
}

10.3 创建大文件上传控制器

src/module/upload/large-file.controller.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { Controller, Post, Req, Res } from '@nestjs/common';
import { Request, Response } from 'express';
import { ApiTags, ApiOperation } from '@nestjs/swagger';
import { LargeFileService } from './large-file.service';

@ApiTags('大文件上传')
@Controller('upload/large')
export class LargeFileController {
constructor(private readonly largeFileService: LargeFileService) {}

@Post('*')
@ApiOperation({ summary: '大文件分片上传' })
async handleUpload(@Req() req: Request, @Res() res: Response) {
return new Promise<void>((resolve, reject) => {
this.largeFileService.getServer().handle(req, res, (error) => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
}
}

11. 路由守卫与拦截器

11.1 日志拦截器

src/common/interceptor/logger.interceptor.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const { method, url, ip, headers } = request;
const userAgent = headers['user-agent'] || '';

const now = Date.now();

return next.handle().pipe(
tap(() => {
const response = context.switchToHttp().getResponse();
const delay = Date.now() - now;

console.log(`${method} ${url} ${response.statusCode} ${delay}ms - ${ip} ${userAgent}`);
}),
);
}
}

11.2 超时拦截器

src/common/interceptor/timeout.interceptor.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, RequestTimeoutException } from '@nestjs/common';
import { Observable, throwError, TimeoutError } from 'rxjs';
import { catchError, timeout } from 'rxjs/operators';

@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
timeout(10000), // 10秒超时
catchError(err => {
if (err instanceof TimeoutError) {
return throwError(() => new RequestTimeoutException());
}
return throwError(() => err);
}),
);
}
}

12. 日志系统

12.1 安装日志依赖

1
npm install nest-winston winston winston-daily-rotate-file

12.2 创建日志配置

src/config/logger.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { utilities as nestWinstonModuleUtilities } from 'nest-winston';
import * as winston from 'winston';
import 'winston-daily-rotate-file';

export const winstonConfig = {
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.timestamp(),
winston.format.ms(),
nestWinstonModuleUtilities.format.nestLike('NestJS', {
colors: true,
prettyPrint: true,
}),
),
}),
new winston.transports.DailyRotateFile({
filename: 'logs/application-%DATE%.log',
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '14d',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json(),
),
}),
],
};

13. 环境配置管理

13.1 创建环境配置

.env.production

1
2
3
4
5
6
7
8
9
10
11
12
13
NODE_ENV=production
PORT=3000

# 生产环境数据库
DB_TYPE=mysql
DB_HOST=production-db-host
DB_PORT=3306
DB_USER=prod_user
DB_PASSWORD=prod_password
DB_DATABASE=prod_nest_serve

JWT_SECRET=production-secret-key
JWT_EXPIRES_IN=1d

.env.test

1
2
3
4
5
6
7
8
9
NODE_ENV=test
PORT=3001

# 测试环境数据库
DB_TYPE=sqlite
DB_DATABASE=test-data/test.db

JWT_SECRET=test-secret-key
JWT_EXPIRES_IN=1h

14. 部署与生产环境

14.1 Docker配置

创建 Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
FROM node:18-alpine

WORKDIR /app

# 复制package文件
COPY package*.json ./

# 安装依赖
RUN npm ci --only=production

# 复制源代码
COPY . .

# 构建应用
RUN npm run build

# 暴露端口
EXPOSE 3000

# 启动应用
CMD ["npm", "run", "start:prod"]

14.2 Docker Compose配置

创建 docker-compose.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
version: '3.8'

services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
depends_on:
- db
restart: unless-stopped

db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root_password
MYSQL_DATABASE: nest_serve
MYSQL_USER: app_user
MYSQL_PASSWORD: app_password
ports:
- "3306:3306"
volumes:
- db_data:/var/lib/mysql
restart: unless-stopped

volumes:
db_data:

14.3 生产环境启动脚本

package.json 中添加:

1
2
3
4
5
6
{
"scripts": {
"start:prod": "cross-env NODE_ENV=production node dist/main",
"build:prod": "npm run build && npm prune --production"
}
}

总结

本教程详细介绍了NestJS项目从零到一的完整开发流程,涵盖了:

  1. 项目初始化 - 脚手架创建和项目结构规划
  2. 数据库配置 - 多数据库连接和实体定义
  3. 数据验证 - DTO封装和验证规则
  4. 认证授权 - JWT实现和角色权限控制
  5. 接口文档 - Swagger自动生成API文档
  6. 文件处理 - 普通上传和大文件分片上传
  7. 中间件 - 拦截器、守卫、过滤器的使用
  8. 日志系统 - 结构化日志记录和日志轮转
  9. 环境配置 - 多环境配置管理
  10. 部署运维 - Docker容器化部署

学习建议

  1. 循序渐进:按照教程顺序逐步学习,不要跳跃
  2. 动手实践:每个章节都要实际编写代码并运行测试
  3. 理解原理:不仅要会使用,还要理解背后的设计思想
  4. 查阅文档:遇到问题时查阅NestJS官方文档
  5. 代码规范:养成良好的代码编写习惯

常见问题解决

Q: 数据库连接失败

A: 检查数据库服务是否启动,配置信息是否正确

Q: JWT令牌无效

A: 检查令牌是否过期,密钥配置是否正确

Q: 文件上传失败

A: 检查上传目录权限,文件大小限制

Q: Swagger文档无法访问

A: 检查Swagger配置路径是否正确

扩展学习

完成本教程后,可以进一步学习:

  1. 微服务架构 - 使用NestJS构建微服务
  2. GraphQL - 替代REST API的查询语言
  3. WebSocket - 实时通信功能
  4. 性能优化 - 缓存、数据库优化等
  5. 测试驱动开发 - 单元测试和集成测试

资源推荐


结语

本教程通过实际项目案例,系统性地讲解了NestJS的核心概念和最佳实践。通过跟随教程学习,你将能够:

✅ 掌握NestJS框架的核心概念和使用方法
✅ 理解模块化开发的思想和实践
✅ 学会数据库连接和ORM的使用
✅ 掌握认证授权和安全防护
✅ 学会API文档的自动生成
✅ 掌握文件上传和大文件处理
✅ 了解生产环境部署和运维

希望本教程能够帮助你快速上手NestJS开发,构建高质量的Node.js应用!

Happy Coding! 🚀

本文作者:辰風依恛
本文链接:https://766187397.github.io/2025/10/12/Nestjs%E9%A1%B9%E7%9B%AE%E9%87%8D%E7%82%B9%E8%AE%B0%E5%BD%95/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可
×