杯子茶室

关注有趣的事物

NestJS入門筆記

网络 0 评

以下是一個試手nestjs的小項目,用於創建一個簡單的用戶信息CRUD API。

# 创建新项目
nest new [项目名称]

# 启动项目
npm start:dev

可以看到生成了一个新的项目,src是我们写代码的地方,结构如下

src
├── app.controller.spec.ts
├── app.controller.ts
├── app.module.ts
├── app.service.ts
└── main.ts

app.module.ts是app模块,其中@Module的input中包含其他模块,一般用cli进行控制,而controllers和providers分别是路由控制器和服务提供器。

以下创建一个新的模块并自动添加到app module中

# 创建模块
nest g module users
# 创建控制器
nest g controller users
# 创建服务
nest g service users

一般来说这就是nest的主要结构了。

撰写控制器逻辑

使用装饰器指定控制器内的路由控制函数

    @Get("/") // 处理Get请求,里面的参数是路径
    findAll(@Query("role") role?: 'INTERN'|"ENGINEER"|"ADMIN") { // 使用@Query装饰变量以获取?请求的内容
        return [{},role] // array of users
    }

    @Get("/:id")
    findOne(@Param('id') id: string) {
        return {id} // user
    }

    @Post()
    create(@Body() user: {}){
        return user
    }

    @Patch("/:id")
    update(@Param('id') id: string, @Body() userUpdate: {}) {
        return {id,...userUpdate} // user
    }

    @Delete("/:id")
    delete(@Param('id') id: string) {
        return {id} // user
    }

NestJS中控制器和服務邏輯是分開的,這意味著使用服務邏輯需要先導入服務。NestJS的控制器在構造器中注入服務邏輯來實現導入服務Class。

import { UsersService } from './users.service';

@Controller('users')
export class UsersController {

    constructor(private readonly usersService: UsersService) {} // 注入UserService,在控制器內可以使用self.usersService調用

    ...

}

至於服務中要怎麼寫,其實相對來說不難。

import { Injectable } from '@nestjs/common';

@Injectable()
export class UsersService {
    private users = [
        {id: 1, name: "Alice", role: "INTERN", email: "1@example.com"},
        {id: 2, name: "Bob", role: "ENGINEER", email: "2@example.com"},
        {id: 3, name: "Charlie", role: "ADMIN", email: "3@example.com"},
    ]
    findAll(role?: 'INTERN'|"ENGINEER"|"ADMIN") {
        if(role){
            return this.users.filter(user => user.role === role)
        }
        return this.users
    }
    findOne(id: number) {
        return this.users.find(user => user.id === id)
    }
    create(user: {name: string, email: string, role: 'INTERN'|"ENGINEER"|"ADMIN"}) {
        const usersByHighestId = this.users.sort((a,b) => b.id - a.id)
        const newUser = {id: usersByHighestId[0].id+1, ...user}
        this.users.push(newUser)
        return newUser
    }
    update(id: number, updatedUser: {name?: string, email?: string, role?: 'INTERN'|"ENGINEER"|"ADMIN"}) {
        this.users = this.users.map(user => {
            if(user.id === id){
                return {...user, ...updatedUser}
            }
            return user
        }
        )
        return this.findOne(id)
    }
    delete(id: number) {
        const removeUser = this.findOne(id)
        this.users = this.users.filter(user => user.id !== id)
        return removeUser
    }
}

下一步是:https://www.youtube.com/watch?v=ZLp92Iw0rkI&list=PL0Zuz27SZ-6MexSAh5x1R3rU6Mg2zYBVr&index=4

DTO

DTO,數據傳輸對象的作用是標準化數據的格式,並啟用數據驗證。

需要使用的功能是NestJS的Pipe,是用於中間件的功能,要麼用於轉換數據,要麼用於驗證數據。

https://nest.nodejs.cn/techniques/validation

先安裝驗證器和轉換器

npm i class-validator class-transformer
// users.controller
import { ...,ParseIntPipe, ValidationPipe } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';

...

    @Get("/:id")
    findOne(@Param('id',ParseIntPipe) id: number) { //使用PraseIntPipe將id轉為number
        return this.usersService.findOne(id)
    }

    @Post()
    create(@Body(ValidationPipe) user: CreateUserDto) { //使用ValidationPipe驗證對象為對應的Dto格式,在user直接使用Dto即可
        return this.usersService.create(user)
    }

    @Patch("/:id")
    update(
        @Param('id',ParseIntPipe) id: number, 
        @Body(ValidationPipe) userUpdate: UpdateUserDto) {
        return this.usersService.update(id, userUpdate)
    }

    @Delete("/:id")
    delete(@Param('id',ParseIntPipe) id: number) {
        return this.usersService.delete(id) // user
    }

Dto的創建就是使用裝飾器賦予每個值的驗證邏輯

// dto/create-user.dto.ts

import { IsEmail, IsEnum, IsNotEmpty, IsString  } from "class-validator";

export class CreateUserDto {
    @IsString()
    @IsNotEmpty()
    name: string;

    @IsEmail()
    email: string;

    @IsEnum(["INTERN","ENGINEER","ADMIN"],{
            message: "Role must be one of 'INTERN', 'ENGINEER', or 'ADMIN'"
    }) //使用這個方式可以在非法DTO的時候返回信息
    role: 'INTERN'|"ENGINEER"|"ADMIN";
}
当你构建 CRUD(创建/读取/更新/删除)等功能时,在基本实体类型上构建变体通常很有用。Nest 提供了几个实用函数来执行类型转换,使这项任务更加方便。
构建输入验证类型(也称为 DTO)时,在同一类型上构建创建和更新变体通常很有用。例如,创建变体可能需要所有字段,而更新变体可能使所有字段可选。
Nest 提供了 PartialType() 实用函数来简化此任务并最大限度地减少样板文件。
PartialType() 函数返回一个类型(类),其中输入类型的所有属性都设置为可选。
import { CreateUserDto } from "./create-user.dto";
import { PartialType } from "@nestjs/mapped-types";

export class UpdateUserDto extends PartialType(CreateUserDto) {} // PartialType

同樣的,修改完controller之後還需要修改service,

// users.service.ts

...

    create(user: CreateUserDto) { // 可以看到CreateUserDto已經作為輸入參數的類型
        const usersByHighestId = this.users.sort((a,b) => b.id - a.id)
        const newUser = {id: usersByHighestId[0].id+1, ...user}
        this.users.push(newUser)
        return newUser
    }
    update(id: number, updatedUser: UpdateUserDto) {
        this.users = this.users.map(user => {
            if(user.id === id){
                return {...user, ...updatedUser}
            }
            return user
        }
        )
        return this.findOne(id)
    }

...

使用PrismaORM連接Postgres數據庫

先安裝prisma到項目中

npm i prisma -D
npm prisma init

編輯.env,你可以自己起一個docker容器進行開發,也可以自己上網找可供開發的postgres服務。

然後編輯prisma/schema.prisma

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model Employee {
  id        Int      @id @default(autoincrement())
  name      String   
  email     String   @unique
  role      Role
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

enum Role {
  INTERN
  ENGINEER
  ADMIN
}

大概看看也知道是什麼意思,不過多解釋了

接下來是如何將prisma應用到項目中。

首先應用prisma到數據庫中

npx prisma migrate dev --name init

如果之後你對以上schema做了任何修改,你可以運行以下腳本

npx prisma generate
npx prisma migrate dev --name change

所有的操作都可以在prisma/migrations/裡面找得到,有原始的sql指令。

現在創建一個database模塊和服務,供其他模塊使用

# 項目根目錄裡面用
nest g module database
nest g service database

然後設置導出database service

// database.module.ts

import { Module } from '@nestjs/common';
import { DatabaseService } from './database.service';

@Module({
  providers: [DatabaseService],
  exports: [DatabaseService],
})
export class DatabaseModule {}

至於模塊怎麼寫,database模塊需要在模塊啟動的時候做一些事情,大概如下

// database.service.ts

import { Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';


@Injectable()
export class DatabaseService extends PrismaClient implements OnModuleInit { // 做了兩件事情
        // 1. DatabaseService作為PrismaClient的子類,意思是模塊的service即為PrismaClient
        // 2. 定義為OnModuleInit的實現
    async onModuleInit() { // 實現這個函數,每次模塊啟動都會執行這個函數
        await this.$connect() // 簡單的連接數據庫
    }
}

我們配置的是Employee的ORM,那麼就需要一個Employees模塊來實現對於Employee數據的CRUD操作。

先創建資源以實現CRUD。

nest g resurces employees
# 會要求選擇是什麼CRUD,默認選擇REST即可。

然後需要在Employees模塊裡面導入database

// employees.module.ts
import { Module } from '@nestjs/common';
import { EmployeesService } from './employees.service';
import { EmployeesController } from './employees.controller';
import { DatabaseModule } from 'src/database/database.module';

@Module({
  imports: [DatabaseModule], // 導入數據庫模塊
  controllers: [EmployeesController],
  providers: [EmployeesService],
})
export class EmployeesModule {}

在創建資源的時候,nestjs會順帶創建需要的dto,其實都不需要了,直接進入employees.controllers.ts和employees.service.ts刪除即可,換成導入Prisma。

// employees.controller.ts

import { Controller, Get, Post, Body, Patch, Param, Delete, Query } from '@nestjs/common';
import { EmployeesService } from './employees.service';
import { Prisma } from '@prisma/client'; // 導入Prisma

@Controller('employees')
export class EmployeesController {
  constructor(private readonly employeesService: EmployeesService) {}

  @Post()
  create(@Body() createEmployeeDto: Prisma.EmployeeCreateInput) { // 使用Prisma定義好的Dto
    return this.employeesService.create(createEmployeeDto);
  }

  @Get()
  findAll(@Query('role') role?: 'INTERN' | 'ENGINEER' | 'ADMIN') {
    return this.employeesService.findAll(role);
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.employeesService.findOne(+id);
  }

  @Patch(':id')
  update(@Param('id') id: string, @Body() updateEmployeeDto: Prisma.EmployeeUpdateInput) { // 使用Prisma定義好的Dto
    return this.employeesService.update(+id, updateEmployeeDto);
  }

  @Delete(':id')
  remove(@Param('id') id: string) {
    return this.employeesService.remove(+id);
  }
}

以下是service

// employees.service.ts

import { Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client'; // 導入Prisma用於提供類型
import { DatabaseService } from 'src/database/database.service'; // 導入數據庫服務用於操作數據
@Injectable()
export class EmployeesService {

  constructor(private readonly databaseService: DatabaseService) {} // 注入數據庫服務,在類中可以直接調用數據庫

  async create(createEmployeeDto: Prisma.EmployeeCreateInput) {
    return this.databaseService.employee.create(
      {data: createEmployeeDto} // 根據Dto創建數據並返回,對於任何CreateInput,Prisma的對應ORM的create方法都可以解析並寫入到數據庫中。
    )
  }

  async findAll(role?: "INTERN" | "ENGINEER" | "ADMIN") {
    return this.databaseService.employee.findMany({
      where: {
        role,
      } // 使用where語句,其他的課件Prisma文檔
    });
    return this.databaseService.employee.findMany()
  }

  async findOne(id: number) {
    return this.databaseService.employee.findUnique({
      where: {
        id,
      }
    });
  }

  async update(id: number, updateEmployeeDto: Prisma.EmployeeUpdateInput) {
    return this.databaseService.employee.update({
      where: {
        id,
      },
      data: updateEmployeeDto, // Update接收包含查詢鍵和data的對象。
    })
  }

  async remove(id: number) {
    return this.databaseService.employee.delete({
      where: {
        id,
      }
    })
  }
}

然後可以執行測試,看看創建的API是否有問題。

數據可視化基礎
发表评论
撰写评论