nestjs学习日记

nestjs学习日记

🗨️ 问一下DeepSeek今天要学什么

安装与运行

1
npm i -g @nestjs/cli

(typescript)安装express的声明文件

1
npm i @types/express

运行(生产)

1
npm start
  • 生产模式启动,适用于部署环境、测试生产环境的启动流程(是运行生产环境代码)
  • 不监听文件变化

运行(开发)

1
npm run start:dev
  • 热更新
  • 详细的日志输出
  • 启动慢

加快构建速度(使用swc

1
npm run start -- -b swc

构建生产环境代码

1
npm run build
  • 将ts代码编译为js代码
  • 后续部署

术语表

  • 装饰器(Decorator):一种函数,使用时需要以@开头放在需要装饰的代码上方,旨在不修改代码的前提下给代码拓展功能

  • JWT

解答

  • 为什么说是集成express,因为例如请求、相应对象是相通的

学习日记

D1

  1. 创建一个模块
  2. 连接数据库
  3. 添加Swagger文档
  4. 错误处理

基础模块

创建一个user模块(生成模块必须文件和可选的.spec.ts的测试文件),可使用nest g --help查看更多命令

1
nest g resource user
  • 一般选择REST API
  • 创建的模块会自动更新app.module.ts文件
  • 此命令不会创建dto文件(用来规范客户端与服务端之间的数据格式),需手动创建
  • 文件一般被放在如user/dto/create-user.dto.ts这里

目录结构

编辑后的代码示例:

user.controller.ts(控制器)接收请求,具体的业务逻辑由use.service.ts文件提供

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { Controller, Get, Post, Body } from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/create-user.dto';

// 当前接口,如 localhost:3000/user
// 此装饰器是控制器必须的
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}

@Get()
getUsers() {
return this.userService.getUsers()
}

// 这里Body装饰器的作用是从 http请求体 中提取数据,并且绑定到方法的参数上
// 并且可讲数据类型自动转换为 dto 中的类型
@Post()
createUser(@Body() createUserDto: CreateUserDto) {
this.userService.createUser(createUserDto)
}
}

use.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 { CreateUserDto } from './dto/create-user.dto';

// 此装饰器是服务必须的
@Injectable()
export class UserService {
private users = [
{ id: 1, name: '小明', age: 18 },
{ id: 2, name: '小白', age: 19 }
]

getUsers() {
return this.users
}

createUser(createUserDto: CreateUserDto) {
const newUser = { id: Date.now(), ...createUserDto }
this.users.push(newUser)
return newUser
}
}

create-user.dto.ts(Data Transfer Object、数据传输对象)可单独作为一个普通dto来使用

1
2
3
4
export class CreateUserDto {
name: string
age: number
}

还可以结合class-validator(需要额外安装)来实现更高效的验证

1
npm install class-validator class-transformer
1
2
3
4
5
6
7
8
9
10
11
12
import { IsString, IsEmail, MinLength } from 'class-validator'

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

age: number

@IsEmail()
email: string
}

连接数据库

使用typeorm连接数据库

1
npm install @nestjs/typeorm

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
import { TypeOrmModule } from '@nestjs/typeorm';

// ...

@Module({
imports: [
UserModule,
AuthModule,
// forRoot 用于设置模块的全局配置选项
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: '123456',
database: 'nestjs',
entities: [User],
synchronize: true,
}),
],
controllers: [AppController],
providers: [AppService],
})

// ...

通过entities创建一个用户表,一般在当前模块下创建,如user/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
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

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

@Column()
name: string;

@Column()
age: number;

@Column()
passwd: string;

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

@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
createTime: Date;
}

随后在user.modules.ts中导入,一旦导入就会自动创建数据表(typeorm配置时,synchronize设为true

1
2
3
4
5
6
7
8
import { User } from './entities/user.entity';

@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UserController],
providers: [UserService],
exports: [UserService],
})

通过接口创建一条数据(控制器 -> 服务),在users.service.ts中通过Repository来执行,它提供了很多操作数据库的方法(find, create等)。有了这个,在绝大多数情况下不需要写SQL

以下代码中,实际起到作用的是InjectRepositoryRepository在此只起到类型的作用

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 { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateUserDto } from './dto/create-user.dto';
import { User } from './entities/user.entity';

@Injectable()
export class UserService {
constructor(
@InjectRepository(User)
private userRepository: Repository<User>,
) { }

getUsers(): Promise<User[]> {
return this.userRepository.find();
}

createUser(createUserDto: CreateUserDto): Promise<User> {
const user = this.userRepository.create(createUserDto);
return this.userRepository.save(user);
}

findOne(username: string) {
const user = this.userRepository.findOne({ where: { name: username } })

return Promise.resolve(user)
}
}

添加Swagger文档

1
npm install @nestjs/swagger

修改main.ts以配置swagger,访问文档http://localhost:3000/api

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';

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

const config = new DocumentBuilder()
.setTitle('NestJS学习')
.setDescription('NestJS学习API文档')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document); // api 指定文档的访问路径

await app.listen(3000);
}

main.ts文件中相当于初始化swagger,具体的接口描述可以使用swagger提供的装饰器在具体的接口中使用

1
2
3
4
5
6
7
8
9
10
11
12
import { ApiTags } from '@nestjs/swagger';

@ApiTags('user')
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}

@Get()
getUsers() {
return this.userService.getUsers()
}
}

错误处理

创建文件src/filters/http-exception.filter.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { ExceptionFilter, Catch, ArgumentsHost } from '@nestjs/common';
import { Response } from 'express';

@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: Error, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();

response.status(500).json({
message: 'Internal Server Error',
error: exception.message,
});
}
}

修改main.ts以全局注册过滤器

1
app.useGlobalFilters(new HttpExceptionFilter());

D2

  1. 用户认证(JWT)
  2. 中间件实战(日志级联)
  3. 环境配置
  4. 单元测试入门
  5. 部署优化(生产环境配置)

JWT

1
2
npm install @nestjs/jwt @nestjs/passport passport passport-jwt bcrypt
npm install @types/passport-jwt @types/bcrypt --save-dev

创建认证模块

1
nest g resource auth

成功创建

实现用户注册(需要创建和数据库对应的entity),如:

src/user/entities/user.entity.ts

1
2
3
4
5
export class User {
id: number
username: string
password: string // 实际保存为哈希值
}

修改auth.service.ts实现登录相关的业务逻辑(获取用户数据,实现成功登录token返回)

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 { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt'
import * as bcrypt from 'bcrypt'
import { UserService } from '../user/user.service';

@Injectable()
export class AuthService {
constructor(private userService: UserService,
private jwtService: JwtService
){}

async validateUser(username: string, pass: string): Promise<any> {
const user = await this.userService.findOne(username)
if (user && (await bcrypt.compare(pass, user.passwd))) {
// 返回除密码外的其他数据(实用的操作)
const { passwd, ...result} = user
return result
}
return null
}

async login(user: any) {
const payload = { username: user.username, sub: user.id }
return {
access_token: this.jwtService.sign(payload)
}
}
}

修改auth.controller.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { Controller, Body, Post, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';

@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}

// dto也不一定需要写在单独的文件,行内也可以
@Post('login')
async login(@Body() loginDto: { username: string; password: string }) {
const user = await this.authService.validateUser(loginDto.username, loginDto.password)
if (!user) throw new UnauthorizedException();
return this.authService.login(user)
}
}

修改auth.module.ts文件配置JWT模块

1
2
3
4
5
6
7
8
9
10
11
12
import { JwtModule } from '@nestjs/jwt';

@Module({
imports: [
JwtModule.register({
secret: 'your_secret_key',
signOptions: { expiresIn: '1h' },
}),
],
providers: [AuthService],
})
export class AuthModule {}

一般也是使用环境变量进行配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';

@Module({
imports: [
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
secret: config.get("JWT_SECRET"),
signOptions: { expiresIn: '1h' },
})
}),
],
providers: [AuthService],
})
export class AuthModule {}

中间件

中间件一般会保存到如:

src/middleware/logger.middleware.ts

1
2
3
4
5
6
7
8
9
10
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) {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
next();
}
}

修改app.module.ts中全局应用中间件

1
2
3
4
5
6
7
8
9
10
import {  NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleWare } from './middleware/logger.middleware';

export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleWare)
.forRoutes('*');
}
}

环境配置

通过.env文件来区分不同环境

1
npm install @nestjs/config

修改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
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config'
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserModule } from './user/user.module';
import { AuthModule } from './auth/auth.module';

// 以下配置也可以直接使用 process.env.xx 来获取
@Module({
imports: [
UserModule,
AuthModule,
ConfigModule.forRoot({
isGlobal: true,
envFilePath: `.env.${process.env.NODE_ENV || 'development'}`
}),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
type: 'mysql',
host: config.get('DB_HOST'),
port: Number(config.get('DB_PORT')),
username: config.get('DB_USER'),
password: config.get('DB_PASSWORD'),
database: config.get('DB_DATABASE'),
entities: [User],
synchronize: true,
}),
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
  • 多数支持动态配置的都会useFactory有这个方法

本地创建两个不同的环境变量文件:.env.development.env.production,尽量确保文件名和NODE_ENV的值对应

1
2
3
4
5
6
7
8
9
# .env.development
NODE_ENV=development
DATABASE_URL=sqlite:./dev.db
JWT_SECRET=development_secret

# .env.production
NODE_ENV=production
DATABASE_URL=mysql://user:password@localhost:3306/prod_db
JWT_SECRET=production_secret

也可以在运行的时候指定环境变量:

1
"start:dev": "NODE_ENV=development nest start --watch"

单元测试

单元测试文件在运行nest g resource xx的时候会自动创建,也可手动创建

例如测试UserService,修改user.service.spec.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 { Test, TestingModule } from '@nestjs/testing';
import { UserService } from './user.service';

describe('UserService', () => {
let service: UserService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [UserService],
}).compile();

service = module.get<UserService>(UserService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});

it('show return users', () => {
expect(service.getUsers()).toEqual([
{ id: 1, name: '小明', age: 18, passwd: '123' },
{ id: 2, name: '小白', age: 19, passwd: '123' }
])
})
});

运行测试

1
npm run test

部署优化

运行npm run start的时候已经是生产环境,除此之外还可以打包之后再部署(可使用swcpm2等进行打包)

安装swc

1
npm install --save-dev @swc/cli @swc/core

在项目根目录下创建.swc文件,并且添加打包命令到 npm-script,如"build": "swc src -d dist"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"jsc": {
"parser": {
"syntax": "typescript",
"tsx": false,
"decorators": true,
"dynamicImport": true
},
"target": "es2020",
"keepClassNames": true,
"transform": {
"legacyDecorator": true,
"decoratorMetadata": true
}
},
"module": {
"type": "commonjs"
}
}

使用docker进行容器部署,在项目根目录创建Dockerfile文件(必须大写开头)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 使用 Node.js 官方镜像
FROM node:16

# 设置工作目录
WORKDIR /app

# 复制 package.json 和 package-lock.json
COPY package*.json ./

# 安装依赖
RUN npm install --production

# 复制源代码
COPY . .

# 构建应用
RUN npm run build

# 暴露端口
EXPOSE 3000

# 启动应用
CMD ["node", "dist/main.js"]

构建镜像和运行容器

1
2
3
4
5
# 构建 Docker 镜像
docker build -t my-nest-app .

# 运行 Docker 容器
docker run -p 3000:3000 my-nest-app

使用nginx进行反向代理(处理跨域的好方法),当访问yourdomain.com/api时代理到本地的3000端口

1
2
3
4
5
6
7
8
9
10
11
server {
listen 80;
server_name yourdomain.com;

location /api {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

D3

  1. 权限守卫
  2. 文件上传与静态资源托管
  3. WebSocket实时通信

权限守卫

创建角色装饰器,新建文件src/auth/roles.decorator.ts

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

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

实现守卫,新建文件src/auth/roles.guard.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;

// 模拟从请求中获取用户角色(实际应从 JWT 解析)
const request = context.switchToHttp().getRequest();
const user = request.user; // 假设 user 已通过 JWT 中间件注入
return requiredRoles.some((role) => user.roles?.includes(role));
}
}

在控制器中使用守卫,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
// src/users/users.controller.ts
import { Roles } from '../auth/roles.decorator';
import { RolesGuard } from '../auth/roles.guard';

@Controller('users')
@UseGuards(RolesGuard) // 全局应用守卫,或在模块中全局注册
export class UserController {
@Get('admin')
@Roles('admin') // 仅允许 admin 角色访问
getAdminData() {
return { message: 'Admin data' };
}
}

文件上传与静态资源托管

文件上传

需要外安装依赖

1
npm install @nestjs/platform-express multer
1
npm install @types/multer --save-dev

创建文件上传控制器:src/files/files.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
import { Controller, Post, UseInterceptors, UploadedFile } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { diskStorage } from 'multer';
import { extname } from 'path';

@Controller('files')
export class FilesController {
@Post('upload')
@UseInterceptors(FileInterceptor('file', {
storage: diskStorage({
destination: './uploads',
filename: (req, file, callback) => {
const randomName = Array(32).fill(null).map(() => Math.round(Math.random() * 16).toString(16)).join('');
callback(null, `${randomName}${extname(file.originalname)}`);
},
}),
}))
uploadFile(@UploadedFile() file: Express.Multer.File) {
return {
url: `/static/${file.filename}`,
};
}
}
  • FileInterceptor:文件上传拦截器(第一个参数是文件字段名(formData中的键名)

托管静态文件,在main.ts中配置

1
2
3
4
5
6
7
8
9
10
11
12
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { join } from 'path';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 静态文件托管, 一般会指定到非源码目录
app.use('/static', express.static(join(__dirname, '..', 'uploads')));
await app.listen(3000);
}
bootstrap();

静态资源托管

除了上面的方式外,常用的处理方法还有

  • 使用 nginx 作为静态文件服务器
  • 使用 CDN 服务(阿里云OOS + CDN、腾讯云COS + CDN)
  • nestjs 自带的ServeStaticModule模块

在 nginx 中配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
server {
listen 80;
server_name yourdomain.com;

location /static/ {
alias /var/www/static/; # 静态资源目录
}

location / {
proxy_pass http://localhost:3000; # 反向代理到 NestJS 应用
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

使用ServeStaticModule,预先安装:

1
npm install @nestjs/serve-static

app.module.ts中配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ServeStaticModule } from '@nestjs/serve-static';
import { join } from 'path';

@Module({
imports: [
ServeStaticModule.forRoot({
rootPath: '/var/www/static', // 静态资源目录
serveRoot: '/static', // 访问路径
}),
],
})
export class AppModule {}
作者

dsjerry

发布于

2025-02-15

更新于

2025-03-30

许可协议

评论