Nest入门笔记①2024版nodejs全栈新兵营
Nest入门笔记①2024版nodejs全栈新兵营
若想提升自身自我,自学是最佳途径。那么如何进行有效的自学呢?首先,可以从一门编程语言的学习开始。然而,许多人由于晦涩难懂的书籍而放弃,再加之庞大的知识量,我们如何能重新找回编程的乐趣呢?正因如此,2024版nodejs全栈新兵营假设你是一个编程新手,将引导你从零开始完成一个小任务。这里不会有晦涩深奥的计算机理论,而只有适合新手的基本操作。当然,在编程世界里,并不存在什么高深的武术,唯有不断深入的基本攻击技巧。
我是ethan_li。正在整理2024版nodejs全栈新兵营课程。如果你对 Node.js全栈学习感兴趣的话,可以关注我,一起交流、学习。wx:ethan5610
安装nestjs
npm i -g @nestjs/cli
软件版本
包 | 版本 |
---|---|
Node.js | v20.11.0 |
npm | 10.2.4 |
nest.js | 10.3.1 |
typescript | 5.3.3 |
创建项目
nest new cats(因为网络问题安装包过程可能会卡顿,可以新建完后进入cats目录,使用npm install 命令安装)
nest new cats ✔ 5695 21:17:55
⚡ We will scaffold your app in a few seconds..
? Which package manager would you ❤️ to use? npm
CREATE cats/.eslintrc.js (663 bytes)
CREATE cats/.prettierrc (51 bytes)
CREATE cats/README.md (3340 bytes)
CREATE cats/nest-cli.json (171 bytes)
CREATE cats/package.json (1944 bytes)
CREATE cats/tsconfig.build.json (97 bytes)
CREATE cats/tsconfig.json (546 bytes)
CREATE cats/src/app.controller.ts (274 bytes)
CREATE cats/src/app.module.ts (249 bytes)
CREATE cats/src/app.service.ts (142 bytes)
CREATE cats/src/main.ts (208 bytes)
CREATE cats/src/app.controller.spec.ts (617 bytes)
CREATE cats/test/jest-e2e.json (183 bytes)
CREATE cats/test/app.e2e-spec.ts (630 bytes)
✔ Installation in progress... ☕
🚀 Successfully created project cats
👉 Get started with the following commands:
$ cd cats
$ npm run start
Thanks for installing Nest 🙏
Please consider donating to our open collective
to help us maintain this package.
🍷 Donate: <https://opencollective.com/nest>
新建的项目下的5个核心文件
src
├── app.controller.spec.ts 带有单个路由的基本控制器示例。
├── app.controller.ts 对于基本控制器的单元测试样例
├── app.module.ts 应用程序的根模块。
├── app.service.ts 带有单个方法的基本服务
└── main.ts 应用程序入口文件。它使用 NestFactory 用来创建 Nest 应用实例。
main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();
Nest 是跨平台框架,有两个支持开箱即用的 HTTP 平台:express 和 fastify,
无论使用哪种平台,它都会暴露自己的 API。 它们分别是 NestExpressApplication
和 NestFastifyApplication
。
基于笔者了解,express更流行,学习资源更丰富,下例使用express平台。
main.ts
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { AppModule } from './app.module';
async function bootstrap() {
//const app = await NestFactory.create(AppModule);
const app = await NestFactory.create<NestExpressApplication>(AppModule);
await app.listen(3000);
}
bootstrap();
app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}
进入项目文件夹,运行命令启动
npm run start
或自动加载启动
npm run start:dev
此命令自动加载。
浏览器查看
创建模块
本例我们要创建一个cats的api,路由类似http://127.0.0.1:3000/api,设置全局路由前缀api
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
**app.setGlobalPrefix('api'); // 全局路由前缀api**
await app.listen(3000);
}
bootstrap();
创建cats模块
nest g mo cats SIGINT(2) ↵ 5700 21:28:40
CREATE src/cats/cats.module.ts (81 bytes)
UPDATE src/app.module.ts (308 bytes)
cats.module.ts
import { Module } from '@nestjs/common';
@Module({})
export class CatsModule {}
更新app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { CatsModule } from './cats/cats.module';
@Module({
imports: [CatsModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
创建控制器cats
nest g co cats ✔ 5701 21:28:56
CREATE src/cats/cats.controller.spec.ts (478 bytes)
CREATE src/cats/cats.controller.ts (97 bytes)
UPDATE src/cats/cats.module.ts (166 bytes)
cats.controller.ts
import { Controller } from '@nestjs/common';
@Controller('cats')
export class CatsController {}
cats.module.ts
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
@Module({
controllers: [CatsController]
})
export class CatsModule {}
创建cats服务
nest g service cats ✔ 5702 21:30:47
CREATE src/cats/cats.service.spec.ts (446 bytes)
CREATE src/cats/cats.service.ts (88 bytes)
UPDATE src/cats/cats.module.ts (240 bytes)
cats.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class CatsService {}
cats.module.ts
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService]
})
export class CatsModule {}
注意: 先创建
Module
, 再创建Controller
和Service
, 这样创建出来的文件在Module
中自动注册,反之,后创建Module,Controller
和Service
,会被注册到外层的app.module.ts
下面我们为子模块cats创建一个service并访问它
cats.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class CatsService {
getHello(): string {
return 'hello cats!';
}
}
cats.controller.ts
import { Controller, Get } from '@nestjs/common';
import { CatsService } from './cats.service';
@Controller('cats')
export class CatsController {
constructor(private readonly catsService: CatsService) {}
@Get()
getHello(): string {
return this.catsService.getHello();
}
}
在浏览器中访问
路由装饰器
Nest.js
中没有单独配置路由的地方,而是使用装饰器。Nest.js
中定义了若干的装饰器用于处理路由。
@Controller('cats')
HTTP方法处理装饰器
@Get
、@Post
、@Put
、@Delete
等众多用于HTTP方法处理装饰器
@Get()
getHello(): string {
return this.catsService.getHello();
}
TypeORM连接数据库
先准备数据库,我们采用mysql5.7
本文采用docker安装mysql
在项目目录下创建docker-compose.yml
services:
db:
container_name: mysql
image: mysql:5.7
volumes:
- mysql_data:/var/lib/mysql
ports:
- '3306:3306'
environment:
- MYSQL_ROOT_PASSWORD=123456
- MYSQL_ROOT_HOST=%
volumes:
mysql_data:
启动mysql
docker-compose up -d
使用命令行或GUI连接数据库,mysql的GUI工具很多,我这里采用了tableplus
创建数据库
配置typeorm
安装依赖包
npm install @nestjs/typeorm typeorm mysql2 -S
将 TypeOrmModule
导入AppModule
app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { CatsModule } from './cats/cats.module';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: '123456',
database: 'cats',
entities: [],
synchronize: true,
}),
CatsModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
警告:设置
synchronize: true
不能被用于生产环境,否则您可能会丢失生产环境数据
在cats目录下创建实体
cats.entity.ts
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class Cat {
@PrimaryGeneratedColumn()
id: number;
@Column({ length: 50 })
name: string;
@Column()
age: number;
@Column()
breed: string;
}
让 TypeORM
知道实体的存在
app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { CatsModule } from './cats/cats.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Cat } from './cats/cats.entity';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: '123456',
database: 'cats',
entities: [Cat],
synchronize: true,
}),
CatsModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
查看数据库同步情况:
我们使用GUI新建记录:
创建service
cats.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Cat } from './cats.entity';
@Injectable()
export class CatsService {
constructor(
@InjectRepository(Cat)
private CatsRepository: Repository<Cat>,
) {}
findOne(id: number): Promise<Cat | null> {
return this.CatsRepository.findOneBy({ id });
}
}
要在导入TypeOrmModule.forFeature
的模块之外使用存储库,则需要重新导出由其生成的提供程序。 您可以通过导出整个模块来做到这一点,如下所示:
cats.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
import { Cat } from './cats.entity';
@Module({
imports: [TypeOrmModule.forFeature([Cat])],
exports: [TypeOrmModule],
controllers: [CatsController],
providers: [CatsService]
})
export class CatsModule {}
在controller中添加findById方法
cats.controller.ts
import { Controller, Get, Param } from '@nestjs/common';
import { CatsService } from './cats.service';
@Controller('cats')
export class CatsController {
constructor(private readonly catsService: CatsService) {}
@Get(':id')
async findById(@Param('id') id) {
return await this.catsService.findOne(id);
}
}
访问它
CRUD 实现
cats.service.ts
文件中实现CRUD
操作
import { ConsoleLogger, HttpException, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { EntityManager, Repository } from 'typeorm';
import { Cat } from './cats.entity';
export interface CatsRo {
list: Cat[];
count: number;
}
@Injectable()
export class CatsService {
constructor(
@InjectRepository(Cat)
private readonly catsRepository: Repository<Cat>,
private readonly entityManager: EntityManager,
) {}
// 创建
async create(cat: Partial<Cat>): Promise<Cat> {
const { name } = cat;
if (!name) {
throw new HttpException('缺少name', 401);
}
const item = await this.catsRepository
.findOne({ where: { name } });
if (item) {
throw new HttpException('name已存在', 401);
}
return await this.catsRepository.save(cat);
}
// 获取列表
async findAll(query): Promise<CatsRo> {
const qb = this.entityManager
.createQueryBuilder(Cat, 'cat');
const count = await qb.getCount();
const { pageNum = 1, pageSize = 10 } = query;
qb.limit(pageSize);
qb.offset(pageSize * (pageNum - 1));
const items = await qb.getMany();
return { list: items, count: count };
}
// 获取指定
async findById(id): Promise<Cat> {
return await this.catsRepository
.findOne({ where: { id } });
}
// 更新
async updateById(id, cat): Promise<Cat> {
const existItem = await this.catsRepository
.findOne({ where: { id } });
console.log(existItem);
if (!existItem) {
throw new HttpException(`id为${id}的不存在`, 401);
}
const updateItem = this.catsRepository
.merge(existItem, cat);
return this.catsRepository.save(updateItem);
}
// 刪除
async remove(id) {
const existItem = await this.catsRepository
.findOne({ where: { id } });
console.log(existItem);
if (!existItem) {
throw new HttpException(`id为${id}的不存在`, 401);
}
return await this.catsRepository.remove(existItem);
}
}
cats.controller.ts
文件中实现调用CRUD
操作
import { CatsService, CatsRo } from './cats.service';
import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common';
@Controller('cats')
export class CatsController {
constructor(private readonly CatsService: CatsService) {}
@Post()
async create(@Body() cat) {
return await this.CatsService.create(cat);
}
@Get()
async findAll(@Query() query): Promise<CatsRo> {
return await this.CatsService.findAll(query);
}
@Get(':id')
async findById(@Param('id') id) {
return await this.CatsService.findById(id);
}
@Put(':id')
async update(@Param('id') id, @Body() cat) {
return await this.CatsService.updateById(id, cat);
}
@Delete(':id')
async remove(@Param('id') id) {
return await this.CatsService.remove(id);
}
}
postman调用
新增
修改
根据id查询
查询列表
根据id删除
接口格式统一
首先定义返回的json格式:
{
"code": 0,
"message": "OK",
"data": {
}
}
请求失败时返回:
{
"code": -1,
"message": "error reason",
"data": {}
}
拦截错误请求
创建一个异常过滤器。该命令的作用是在应用程序中添加一个名为 http-exception
的异常过滤器,用于处理 HTTP 异常:
nest g filter core/filter/http-exception SIGINT(2) ↵ 5727 22:06:29
CREATE src/core/filter/http-exception/http-exception.filter.spec.ts (201 bytes)
CREATE src/core/filter/http-exception/http-exception.filter.ts (195 bytes)
实现
import {ArgumentsHost,Catch, ExceptionFilter, HttpException}
from '@nestjs/common';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp(); // 获取请求上下文
const response = ctx.getResponse(); // 获取请求上下文中的 response对象
const status = exception.getStatus(); // 获取异常状态码
// 设置错误信息
const message = exception.message
? exception.message
: `${status >= 500 ? 'Service Error' : 'Client Error'}`;
const errorResponse = {
data: {},
message: message,
code: -1,
};
// 设置返回的状态码, 请求头,发送错误信息
response.status(status);
response.header('Content-Type', 'application/json; charset=utf-8');
response.send(errorResponse);
}
}
需要在main.ts
中全局注册
import { HttpExceptionFilter }
from './core/filter/http-exception/http-exception.filter';
...
app.useGlobalFilters(new HttpExceptionFilter());
这样对请求错误就可以统一的返回了,返回请求错误只需要抛出异常即可,比如之前的:
throw new HttpException('文章已存在', 401);
创建一个拦截器
npm install rxjs -S
nest g interceptor core/interceptor/transform ✔ 5728 22:06:35
CREATE src/core/interceptor/transform/transform.interceptor.spec.ts (204 bytes)
CREATE src/core/interceptor/transform/transform.interceptor.ts (315 bytes)
实现:
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { map, Observable } from 'rxjs';
interface Response<T> {
data: T;
}
@Injectable()
export class TransformInterceptor<T>
implements NestInterceptor<T, Response<T>>
{
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map((data) => {
return {
data,
code: 0,
msg: '请求成功',
};
}),
);
}
}
main.ts
中全局注册:
import { TransformInterceptor }
from './core/interceptor/transform/transform.interceptor';
...
app.useGlobalInterceptors(new TransformInterceptor());
过滤器和拦截器实现都是三部曲:创建 > 实现 > 注册
再试试接口,看看返回的数据格式是不是规范了?
配置接口文档Swagger
npm install @nestjs/swagger swagger-ui-express -S
配置接口main.ts
...
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
...
// 设置swagger文档
const config = new DocumentBuilder()
.setTitle('管理后台')
.setDescription('管理后台接口文档')
.setVersion('1.0')
.addBearerAuth()
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('docs', app, document);
...
配置完成,我们就可以访问:http://localhost:3000/docs
,此时就能看到Swagger
生成的文档
接口说明
import { ApiTags,ApiOperation } from '@nestjs/swagger';
...
@ApiOperation({ summary: '创建' })
@Post()
接口传参
在cats
目录下创建一个dto
文件夹,再创建一个create-cat.dot.ts
文件:
import { IsNotEmpty, IsNumber, IsString } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class CreateCatDto {
readonly name: string;
readonly breed: string;
readonly age: number;
}
然后在Controller
中对创建文章是传入的参数进行类型说明:
// cats.controller.ts
...
import { CreateCatDto } from './dto/create-cat.dto';
async create(@Body() post: CreateCatDto) {...}
数据验证
Nest.js
自带了三个开箱即用的管道:ValidationPipe
、ParseIntPipe
和ParseUUIDPipe
, 其中ValidationPipe
配合class-validator
就可以完美的实现我们想要的效果(对参数类型进行验证,验证失败抛出异常)。
管道验证操作通常用在dto
这种传输层的文件中,用作验证操作。首先我们安装两个需要的依赖包:class-transformer
和class-validator
npm install class-validator class-transformer -S
然后在/cats/dto/create-post.dto.ts
文件中添加验证, 完善错误信息提示:
import { IsNotEmpty, IsNumber, IsString } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class CreatePostDto {
@ApiProperty({ description: '文章标题' })
@IsNotEmpty({ message: '文章标题必填' })
@IsString({ message: '类型要求是string' })
readonly title: string;
@IsNotEmpty({ message: '缺少作者信息' })
@ApiProperty({ description: '作者' })
readonly author: string;
@ApiPropertyOptional({ description: '内容' })
readonly content: string;
@ApiPropertyOptional({ description: '文章封面' })
readonly cover_url: string;
@IsNumber()
@ApiProperty({ description: '文章类型' })
readonly type: number;
}
最后我们还有一个重要的步骤, 就是在main.ts
中全局注册一下管道ValidationPipe
:
import { ValidationPipe } from '@nestjs/common';
...
app.useGlobalPipes(new ValidationPipe());
修改验证 ./core/filter/http-exception/http-exception.filter
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
} from '@nestjs/common';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp(); // 获取请求上下文
const response = ctx.getResponse(); // 获取请求上下文中的 response对象
const status = exception.getStatus(); // 获取异常状态码
const exceptionResponse: any = exception.getResponse();
let validMessage = '';
for (const key in exception) {
console.log(key, exception[key]);
}
if (typeof exceptionResponse === 'object') {
validMessage =
typeof exceptionResponse.message === 'string'
? exceptionResponse.message
: exceptionResponse.message[0];
}
const message = exception.message
? exception.message
: `${status >= 500 ? 'Service Error' : 'Client Error'}`;
const errorResponse = {
data: {},
message: validMessage || message,
code: -1,
};
// 设置返回的状态码, 请求头,发送错误信息
response.status(status);
response.header('Content-Type', 'application/json; charset=utf-8');
response.send(errorResponse);
}
}
参考链接:
转载自:https://juejin.cn/post/7330503771075444787