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
}

添加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 {}

中间件

中间件一般会保存到如:

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();
}
}

修改mian.ts全局应用中间件

1
2
3
import { LoggerMiddleWare } from './middleware/logger.middleware';

app.use(LoggerMiddleware)

环境配置

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

@Module({
imports: [UserModule, AuthModule, ConfigModule.forRoot({
isGlobal: true,
envFilePath: `.env.${process.env.NODE_ENV || 'development'}`
})],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

本地创建两个不同的环境变量文件:.env.development.env.production

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

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

单元测试

单元测试文件在运行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;
}
}
作者

dsjerry

发布于

2025-02-15

更新于

2025-02-22

许可协议

评论