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

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

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

安装nestjs

css 复制代码
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 命令安装)

bash 复制代码
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个核心文件

css 复制代码
src
 ├── app.controller.spec.ts  带有单个路由的基本控制器示例。
 ├── app.controller.ts       对于基本控制器的单元测试样例
 ├── app.module.ts           应用程序的根模块。
 ├── app.service.ts          带有单个方法的基本服务
 └── main.ts                 应用程序入口文件。它使用 NestFactory 用来创建 Nest 应用实例。

main.ts

javascript 复制代码
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 平台:expressfastify

无论使用哪种平台,它都会暴露自己的 API。 它们分别是 NestExpressApplicationNestFastifyApplication

基于笔者了解,express更流行,学习资源更丰富,下例使用express平台。

main.ts

javascript 复制代码
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

python 复制代码
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

typescript 复制代码
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();
  }
}

进入项目文件夹,运行命令启动

arduino 复制代码
npm run start

或自动加载启动

arduino 复制代码
npm run start:dev

此命令自动加载。

浏览器查看

创建模块

本例我们要创建一个cats的api,路由类似http://127.0.0.1:3000/api,设置全局路由前缀api

javascript 复制代码
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模块

arduino 复制代码
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

kotlin 复制代码
import { Module } from '@nestjs/common';

@Module({})
export class CatsModule {}

更新app.module.ts

python 复制代码
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

bash 复制代码
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

kotlin 复制代码
import { Controller } from '@nestjs/common';

@Controller('cats')
export class CatsController {}

cats.module.ts

python 复制代码
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';

@Module({
  controllers: [CatsController]
})
export class CatsModule {}

创建cats服务

bash 复制代码
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

kotlin 复制代码
import { Injectable } from '@nestjs/common';

@Injectable()
export class CatsService {}

cats.module.ts

python 复制代码
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

kotlin 复制代码
import { Injectable } from '@nestjs/common';

@Injectable()
export class CatsService {
  getHello(): string {
    return 'hello cats!';
  }
}

cats.controller.ts

typescript 复制代码
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中定义了若干的装饰器用于处理路由。

kotlin 复制代码
@Controller('cats')

HTTP方法处理装饰器

@Get@Post@Put@Delete等众多用于HTTP方法处理装饰器

kotlin 复制代码
@Get()
  getHello(): string {
    return this.catsService.getHello();
  }

TypeORM连接数据库

先准备数据库,我们采用mysql5.7

本文采用docker安装mysql

在项目目录下创建docker-compose.yml

yaml 复制代码
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

安装依赖包

bash 复制代码
npm install @nestjs/typeorm typeorm mysql2 -S

TypeOrmModule 导入AppModule

app.module.ts

php 复制代码
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

kotlin 复制代码
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

php 复制代码
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

typescript 复制代码
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

python 复制代码
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

typescript 复制代码
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操作

typescript 复制代码
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操作

less 复制代码
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格式:

css 复制代码
{
    "code": 0,
    "message": "OK",
    "data": {
    }
}

请求失败时返回:

css 复制代码
{
    "code": -1,
    "message": "error reason",
    "data": {}
}

拦截错误请求

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

python 复制代码
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)

实现

typescript 复制代码
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中全局注册

javascript 复制代码
import { HttpExceptionFilter } 
	from './core/filter/http-exception/http-exception.filter';

...
app.useGlobalFilters(new HttpExceptionFilter());

这样对请求错误就可以统一的返回了,返回请求错误只需要抛出异常即可,比如之前的:

arduino 复制代码
throw new HttpException('文章已存在', 401);

创建一个拦截器

bash 复制代码
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)

实现:

kotlin 复制代码
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中全局注册:

javascript 复制代码
import { TransformInterceptor } 
	from './core/interceptor/transform/transform.interceptor';

...

app.useGlobalInterceptors(new TransformInterceptor());

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

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

配置接口文档Swagger

bash 复制代码
npm install @nestjs/swagger swagger-ui-express -S

配置接口main.ts

javascript 复制代码
...
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生成的文档

接口说明

css 复制代码
import { ApiTags,ApiOperation } from '@nestjs/swagger';

...

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

接口传参

cats目录下创建一个dto文件夹,再创建一个create-cat.dot.ts文件:

typescript 复制代码
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中对创建文章是传入的参数进行类型说明:

less 复制代码
//  cats.controller.ts
...
import { CreateCatDto } from './dto/create-cat.dto';

async create(@Body() post: CreateCatDto) {...}

数据验证

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

管道验证操作通常用在dto这种传输层的文件中,用作验证操作。首先我们安装两个需要的依赖包:class-transformerclass-validator

arduino 复制代码
npm install class-validator class-transformer -S

然后在/cats/dto/create-post.dto.ts文件中添加验证, 完善错误信息提示:

less 复制代码
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

javascript 复制代码
import { ValidationPipe } from '@nestjs/common';
...
app.useGlobalPipes(new ValidationPipe());

修改验证 ./core/filter/http-exception/http-exception.filter

ini 复制代码
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);
  }
}

参考链接:

  1. juejin.cn/post/703207...
  2. juejin.cn/post/707884...
相关推荐
Never_Satisfied2 小时前
在JavaScript / Node.js中,package.json文件中的依赖项自动选择最新版安装
javascript·node.js·json
yhole4 小时前
如何升级node.js版本
node.js
Luna-player4 小时前
vue3,单页应用学习笔记
node.js
天远云服5 小时前
天远企业司法认证API对接实战:PHP构建B2B供应链合规防火墙
大数据·开发语言·后端·node.js·php
lzp07917 小时前
如何在Windows系统上安装和配置Node.js及Node版本管理器(nvm)
windows·node.js
weiwx8310 小时前
【前端】Node.js使用教程
前端·node.js·vim
i建模11 小时前
Ubuntu Node.js 升级方案
linux·运维·ubuntu·node.js
结网的兔子13 小时前
前端学习笔记(实战准备篇)——用vite构建一个项目【吐血整理】
前端·学习·elementui·npm·node.js·vue
i建模13 小时前
npm国内镜像源加速
前端·npm·node.js
热爱生活的五柒1 天前
解决 npm install 一直在转圈的问题
前端·npm·node.js