likes
comments
collection
share

Nest入门笔记①2024版nodejs全栈新兵营

作者站长头像
站长
· 阅读数 5

Nest入门笔记①2024版nodejs全栈新兵营

若想提升自身自我,自学是最佳途径。那么如何进行有效的自学呢?首先,可以从一门编程语言的学习开始。然而,许多人由于晦涩难懂的书籍而放弃,再加之庞大的知识量,我们如何能重新找回编程的乐趣呢?正因如此,2024版nodejs全栈新兵营假设你是一个编程新手,将引导你从零开始完成一个小任务。这里不会有晦涩深奥的计算机理论,而只有适合新手的基本操作。当然,在编程世界里,并不存在什么高深的武术,唯有不断深入的基本攻击技巧。

我是ethan_li。正在整理2024版nodejs全栈新兵营课程。如果你对 Node.js全栈学习感兴趣的话,可以关注我,一起交流、学习。wx:ethan5610

安装nestjs

npm i -g @nestjs/cli

软件版本

版本
Node.jsv20.11.0
npm10.2.4
nest.js10.3.1
typescript5.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

此命令自动加载。

浏览器查看

Nest入门笔记①2024版nodejs全栈新兵营

创建模块

本例我们要创建一个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) ↵  570021: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, 再创建ControllerService, 这样创建出来的文件在Module中自动注册,反之,后创建Module, ControllerService,会被注册到外层的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入门笔记①2024版nodejs全栈新兵营

路由装饰器

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

Nest入门笔记①2024版nodejs全栈新兵营

创建数据库

Nest入门笔记①2024版nodejs全栈新兵营

配置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 {}

查看数据库同步情况:

Nest入门笔记①2024版nodejs全栈新兵营

我们使用GUI新建记录:

Nest入门笔记①2024版nodejs全栈新兵营

创建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);
  }
}

访问它

Nest入门笔记①2024版nodejs全栈新兵营

Nest入门笔记①2024版nodejs全栈新兵营

使用配置文件

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调用

新增

Nest入门笔记①2024版nodejs全栈新兵营

修改

Nest入门笔记①2024版nodejs全栈新兵营

根据id查询

Nest入门笔记①2024版nodejs全栈新兵营

查询列表

Nest入门笔记①2024版nodejs全栈新兵营

根据id删除

Nest入门笔记①2024版nodejs全栈新兵营

接口格式统一

首先定义返回的json格式:

{
    "code": 0,
    "message": "OK",
    "data": {
    }
}

请求失败时返回:


{
    "code": -1,
    "message": "error reason",
    "data": {}
}

拦截错误请求

创建一个异常过滤器。该命令的作用是在应用程序中添加一个名为 http-exception 的异常过滤器,用于处理 HTTP 异常:

nest g filter core/filter/http-exception                                 SIGINT(2) ↵  572722: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());

过滤器和拦截器实现都是三部曲:创建 > 实现 > 注册

再试试接口,看看返回的数据格式是不是规范了?

Nest入门笔记①2024版nodejs全栈新兵营

配置接口文档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生成的文档

Nest入门笔记①2024版nodejs全栈新兵营

接口说明

import { ApiTags,ApiOperation } from '@nestjs/swagger';

...

  @ApiOperation({ summary: '创建' })
  @Post()

Nest入门笔记①2024版nodejs全栈新兵营

接口传参

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入门笔记①2024版nodejs全栈新兵营

数据验证

Nest.js自带了三个开箱即用的管道:ValidationPipeParseIntPipeParseUUIDPipe, 其中ValidationPipe 配合class-validator就可以完美的实现我们想要的效果(对参数类型进行验证,验证失败抛出异常)。

管道验证操作通常用在dto这种传输层的文件中,用作验证操作。首先我们安装两个需要的依赖包:class-transformerclass-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);
  }
}

参考链接: