所有文章 > API开发 > 从零开始使用Nest JS(结合Prisma和PostgreSQL)构建完整的REST API功能——适合初学者——第1部分

从零开始使用Nest JS(结合Prisma和PostgreSQL)构建完整的REST API功能——适合初学者——第1部分

我一直在寻找一个很棒、很好、很棒的 Node JS 框架来构建 API。经过一番探索,我发现了 Nest JS,它非常棒。它是一个 Node JS 框架,具有许多用 Typescript 编写的内置功能。

在本文中,我将展示如何创建具有以下功能的 REST API:

  • 使用 Prisma 的 CRUD(创建、读取、更新、删除)、
  • 验证
  • 认证
  • 路线保护 /
  • 处理多个 .env 文件,
  • 单元测试、
  • E2e 检验 /
  • 用于文档的开放 API, 等。

整个系列的完整代码可以在这里访问:https://github.com/alfism1/nestjs-api

好了,现在让我们开始吧。

首先,让我们创建一个名为 superb-api 的新 Nest JS 项目。

$ npm i -g @nestjs/cli
$ nest new superb-api

它将提示一个问题来选择包管理器(npm、yarn 或 pnpm)。选择您想要的任何一个。

项目初始化完成后,转到项目文件夹,打开代码编辑器并运行它。

$ cd superb-api
$ npm run start

首次应用程序运行

默认情况下,Nest JS 使用 3000 作为默认端口。

世界您好

如果要更改端口号,只需打开并更新端口号 .src/main.tsawait app.listen(ANY_AVAILABLE_PORT_NUMBER);

Nest JS 概述

现在,让我们快速了解一下项目文件夹结构。

我们的大部分代码都将写入该文件夹。让我们看一下文件夹。共有 5 个文件:srcsrc

  • app.controller.ts:具有单个路由的基本控制器。
  • app.controller.spec.ts:控制器的单元测试。
  • app.module.ts:应用程序的根模块。
  • app.service.ts:具有单一方法的基本服务。
  • main.ts:使用核心函数 NestFactory 创建 Nest 应用程序实例的应用程序的入口文件。

控制器

控制器负责处理传入请求和返回响应。根据 Nest JS 规则,控制器将在带有 decorator 的类中编写。我们还可以在 controller 装饰器中添加一个字符串作为前缀,那么 URL 将是 .在控制器类中,将有具有 HTTP 方法装饰器、、 的函数。与控制器类似,HTTP 方法装饰器也可以接收字符串,例如 ,然后端点 URL 将为 .@Controller()@Controller('some-prefix')domain.com/some-prefix@GET()@POST()@PUT()@PATCH()@DELETE()@GET('test')domain.com/some-prefix/test

import { Controller, Get } from '@nestjs/common';

// Import the AppService class, which is defined in the app.service.ts file.
import { AppService } from './app.service';

// Controller decorator, which is used to define a basic route.
@Controller()
export class AppController {

// Get decorator, which is a method decorator that defines a basic GET route.
@Get()
getHello(): string {
// controller process goes here...
}
}

服务/提供商

服务或提供商负责处理主要流程。我们将在此处编写所有端点逻辑。根据 Nest JS 规则,provider 将在带有 decorator 的类中编写。@Injectable()

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

@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

在上面的代码中,我们有负责返回字符串 ‘Hello World!’ 的方法。然后我们可以将这个 provider 注入到控制器中getHello

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
// inject the AppService into the AppController
constructor(private readonly appService: AppService) {}

@Get('test')
getHello(): string {
// call the AppService's getHello method
return this.appService.getHello();
}
}

模块

在 NestJS 中,模块是一个带有装饰器注解的类。它用于将代码组织成内聚的功能块。模块有几个关键职责,例如定义提供者(服务、工厂、仓库等)、控制器和其他相关内容。它们还允许定义导入和导出,用于管理应用程序不同部分之间的依赖关系。@Module()

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

让我们构建我们出色的 API

Prisma 安装

我们将使用 Prisma 作为数据库 ORM。Prisma 是一个用于处理数据库的出色库。它支持 PostgreSQL、MySQL、SQL Server、SQLite、MongoDB 和 CockroachDB。

首先,让我们在我们的项目中安装 Prisma 包

$ npm install prisma --save-dev

安装 prisma 后,运行以下命令

$ npx prisma init

它将在文件夹内创建文件schema.prismaprisma

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

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

和根文件夹中的文件.env

DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"

确保在文件中设置正确的。DATABASE_URL.env

接下来,打开 和 让我们定义模型架构schema.prisma

model User {
id Int @id @default(autoincrement())
email String @unique
password String
name String?
posts Post[]
}

model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean? @default(false)
author User? @relation(fields: [authorId], references: [id])
authorId Int?
}

创建 Schema 后,通过执行

npx prisma migrate dev --name init

并检查数据库。它将创建以下表:

新表

安装 prisma 客户端

npm install @prisma/client

请注意,在安装过程中,Prisma 会自动为您调用该命令。将来,您需要在每次更改 Prisma 模型后运行此命令,以更新生成的 Prisma Client。prisma generate

该命令读取您的 Prisma 架构并更新其中生成的 Prisma 客户端库prisma generatenode_modules/@prisma/client

接下来,让我们创建一个名为 within directory 的新文件:prisma.service.tssrc/core/services/

// src/core/services/prisma.service.ts

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

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
async onModuleInit() {
await this.$connect();
}
async onModuleDestroy() {
await this.$disconnect();
}
}

对于 Prisma 的最后一个配置,让我们注册为全局模块,以便它可以在任何其他模块上使用。创建一个名为 inside 的新文件PrismaServicecore.module.tssrc/core/

// src/core/core.module.ts

import { Global, Module } from '@nestjs/common';
import { PrismaService } from './services/prisma.service';

@Global()
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class CoreModule {}

然后通过添加 :app.module.tsCoreModule

// src/app.module.ts

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UsersModule } from './modules/users/users.module';
import { CoreModule } from './core/core.module';

@Module({
imports: [UsersModule, CoreModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

现在,我们已准备好与数据库进行交互:)


验证配置

Nest JS 可以很好地进行请求体和查询验证。
首先,安装软件包:

$ npm i --save class-validator class-transformer
$ npm install @nestjs/mapped-types

并更新我们的main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
const app = await NestFactory.create(AppModule);

// Global pipe that will be applied to all routes
// This will validate the request body against the DTO
app.useGlobalPipes(new ValidationPipe());

await app.listen(3000);
}
bootstrap();

我们添加了以使其全局应用。app.useGlobalPipes(new ValidationPipe());main.ts


创建用户终端节点

让我们继续创建终端节点。
在该文件夹中,创建一个名为 的新文件夹。srcmodules/users

在 中,我们将创建处理与用户模型相关的任何逻辑的端点,例如;Create User、Update User 和 Delete User。modules/users

在深入研究代码之前,让我们安装一些包:

bcrypt
我们将在创建用户时加密用户的密码。

$ npm i bcrypt
$ npm install --save @types/bcrypt

现在让我们创建一个新文件夹并创建这些文件:src/modules/users/dtos

  • create-user.dto.ts 用户创建/注册验证
import { IsEmail, IsNotEmpty } from 'class-validator';

export class CreateUserDto {
@IsNotEmpty()
@IsEmail()
email: string;

@IsNotEmpty()
password: string;

@IsNotEmpty()
name: string;
}
  • update-user.dto.ts 用户更新验证
import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';

export class UpdateUsertDto extends PartialType(CreateUserDto) {}
  • login-user.dto.ts 登录验证
import { IsEmail, IsNotEmpty } from 'class-validator';

export class LoginUserDto {
@IsNotEmpty()
@IsEmail()
email: string;

@IsNotEmpty()
password: string;
}

让我们从创建一个名为users.controller.ts

import {
  Body,
Controller,
Delete,
Get,
Param,
ParseIntPipe,
Patch,
Post,
} from '@nestjs/common';
import { CreateUserDto } from './dtos/create-user.dto';
import { UpdateUsertDto } from './dtos/update-user.dto';
import { LoginUserDto } from './dtos/login-user.dto';

@Controller('users')
export class UsersController {
@Post('register')
registerUser(@Body() createUserDto: CreateUserDto): string {
console.log(createUserDto);
return 'Post User!';
}

@Post('login')
loginUser(@Body() loginUserDto: LoginUserDto): string {
console.log(loginUserDto);
return 'Login User!';
}

@Get('me')
me(): string {
return 'Get my Profile!';
}

@Patch(':id')
updateUser(
@Param('id', ParseIntPipe) id: number,
@Body() updateUserDto: UpdateUsertDto,
): string {
console.log(updateUserDto);
return `Update User ${id}!`;
}

@Delete(':id')
deleteUser(@Param('id', ParseIntPipe) id: number): string {
return `Delete User ${id}!`;
}
}

users.controller.ts有 5 个端点:

  • POST /users/register对于用户注册过程,
  • POST /users/login对于用户登录过程,
  • GET /users/me以获取用户的个人资料,
  • PATCH /users/:id对于用户更新,
  • DELETE /users/:id对于用户删除。

上述每个端点都已根据 DTO 实施了验证,例如:

  • 注册输入验证 (DTOcreate-user.dto)
  • 登录输入验证 (DTOlogin-user.dto)
  • 更新用户错误(应为数字):id

现在让我们跳转到 user 服务。在该服务中,我们将创建函数以通过 Prisma 与数据库交互,它们将代表上述每个终端节点。

以下是样板:users.service

import { ConflictException, HttpException, Injectable } from '@nestjs/common';
import { User } from '@prisma/client';
import { PrismaService } from 'src/core/services/prisma.service';
import { CreateUserDto } from './dtos/create-user.dto';
import { hash } from 'bcrypt';

@Injectable()
export class UsersService {
constructor(private prisma: PrismaService) {}

// async registerUser

// async loginUser

// async updateUser

// async deleteUser
}

让我们先从 register 函数开始:

async registerUser(createUserDto: CreateUserDto): Promise<User> {
  try {
// create new user using prisma client
const newUser = await this.prisma.user.create({
data: {
email: createUserDto.email,
password: await hash(createUserDto.password, 10), // hash user's password
name: createUserDto.name,
},
});

// remove password from response
delete newUser.password;

return newUser;
} catch (error) {
// check if email already registered and throw error
if (error.code === 'P2002') {
throw new ConflictException('Email already registered');
}

// throw error if any
throw new HttpException(error, 500);
}
}

registerUser将负责处理 register logic。我们可以看到,现在我们使用 prisma 与数据库交互。我们还对密码进行加密,以确保我们的 API 安全,并将其从响应 API 中删除。this.prisma.user.createhash(createUserDto.password, 10)delete newUser.password

我们还使用 如果存在重复的电子邮件或任何其他错误 来处理错误。throw new ConflictExceptionthrow new HttpException

现在让我们继续登录函数
登录响应将返回一个具有名为 () 的单个属性的对象,其中包含一些使用 生成的用户数据 (),因此我们需要先安装:access_tokeninterface LoginResponseinterface UserPaylodjwt@nestjs/jwt

$ npm install --save @nestjs/jwt

安装后,通过添加@nestjs/jwtconstructorprivate jwtService: JwtService,

constructor(
  private prisma: PrismaService,
private jwtService: JwtService,
) {}

最后但并非最不重要的一点是,通过将 jwt 模块添加为全局来更新我们的:app.module.ts

// src/app.module.ts

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UsersModule } from './modules/users/users.module';
import { CoreModule } from './core/core.module';
import { JwtModule } from '@nestjs/jwt';

@Module({
imports: [
UsersModule,
CoreModule,
// add jwt module
JwtModule.register({
global: true,
secret: 'super_secret_key',
signOptions: { expiresIn: '12h' },
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

请注意,和 应该存储在一个安全的地方,比如 file.

配置完成后,让我们创建一个名为 :jwtsrc/modules/users/interfaces/users-login.interface.ts

export interface UserPayload {
  sub: number;
name: string;
email: string;
}

export interface LoginResponse {
access_token: string;
}

这是代码:loginUser

async loginUser(loginUserDto: LoginUserDto): Promise<LoginResponse> {
    try {
// find user by email
const user = await this.prisma.user.findUnique({
where: { email: loginUserDto.email },
});

// check if user exists
if (!user) {
throw new NotFoundException('User not found');
}

// check if password is correct by comparing it with the hashed password in the database
if (!(await compare(loginUserDto.password, user.password))) {
throw new UnauthorizedException('Invalid credentials');
}

const payload: UserPayload = {
// create payload for JWT
sub: user.id, // sub is short for subject. It is the user id
email: user.email,
name: user.name,
};

return {
// return access token
access_token: await this.jwtService.signAsync(payload),
};
} catch (error) {
// throw error if any
throw new HttpException(error, 500);
}
}

到目前为止,我们已经创建了两个函数,和 .现在让我们继续 和 。registerUserloginUserupdateUserdeleteUser

这是函数:updateUser

async updateUser(id: number, updateUserDto: UpdateUsertDto): Promise<User> {
  try {
// find user by id. If not found, throw error
await this.prisma.user.findUniqueOrThrow({
where: { id },
});

// update user using prisma client
const updatedUser = await this.prisma.user.update({
where: { id },
data: {
...updateUserDto,
// if password is provided, hash it
...(updateUserDto.password && {
password: await hash(updateUserDto.password, 10),
}),
},
});

// remove password from response
delete updatedUser.password;

return updatedUser;
} catch (error) {
// check if user not found and throw error
if (error.code === 'P2025') {
throw new NotFoundException(`User with id ${id} not found`);
}

// check if email already registered and throw error
if (error.code === 'P2002') {
throw new ConflictException('Email already registered');
}

// throw error if any
throw new HttpException(error, 500);
}
}

和最后一个函数 :deleteUser

async deleteUser(id: number): Promise<string> {
  try {
// find user by id. If not found, throw error
const user = await this.prisma.user.findUniqueOrThrow({
where: { id },
});

// delete user using prisma client
await this.prisma.user.delete({
where: { id },
});

return `User with id ${user.id} deleted`;
} catch (error) {
// check if user not found and throw error
if (error.code === 'P2025') {
throw new NotFoundException(`User with id ${id} not found`);
}

// throw error if any
throw new HttpException(error, 500);
}
}

现在 已完成,这是最终的完整代码:UsersService

// src/modules/users/users.service.ts

import {
ConflictException,
HttpException,
Injectable,
NotFoundException,
UnauthorizedException,
} from '@nestjs/common';
import { User } from '@prisma/client';
import { PrismaService } from 'src/core/services/prisma.service';
import { CreateUserDto } from './dtos/create-user.dto';
import { compare, hash } from 'bcrypt';
import { LoginUserDto } from './dtos/login-user.dto';
import { JwtService } from '@nestjs/jwt';
import { LoginResponse, UserPayload } from './interfaces/users-login.interface';
import { UpdateUsertDto } from './dtos/update-user.dto';

@Injectable()
export class UsersService {
constructor(
private prisma: PrismaService,
private jwtService: JwtService,
) {}

async registerUser(createUserDto: CreateUserDto): Promise<User> {
try {
// create new user using prisma client
const newUser = await this.prisma.user.create({
data: {
email: createUserDto.email,
password: await hash(createUserDto.password, 10), // hash user's password
name: createUserDto.name,
},
});

// remove password from response
delete newUser.password;

return newUser;
} catch (error) {
// check if email already registered and throw error
if (error.code === 'P2002') {
throw new ConflictException('Email already registered');
}

// throw error if any
throw new HttpException(error, 500);
}
}

async loginUser(loginUserDto: LoginUserDto): Promise<LoginResponse> {
try {
// find user by email
const user = await this.prisma.user.findUnique({
where: { email: loginUserDto.email },
});

// check if user exists
if (!user) {
throw new NotFoundException('User not found');
}

// check if password is correct by comparing it with the hashed password in the database
if (!(await compare(loginUserDto.password, user.password))) {
throw new UnauthorizedException('Invalid credentials');
}

const payload: UserPayload = {
// create payload for JWT
sub: user.id, // sub is short for subject. It is the user id
email: user.email,
name: user.name,
};

return {
// return access token
access_token: await this.jwtService.signAsync(payload),
};
} catch (error) {
// throw error if any
throw new HttpException(error, 500);
}
}

async updateUser(id: number, updateUserDto: UpdateUsertDto): Promise<User> {
try {
// find user by id. If not found, throw error
await this.prisma.user.findUniqueOrThrow({
where: { id },
});

// update user using prisma client
const updatedUser = await this.prisma.user.update({
where: { id },
data: {
...updateUserDto,
// if password is provided, hash it
...(updateUserDto.password && {
password: await hash(updateUserDto.password, 10),
}),
},
});

// remove password from response
delete updatedUser.password;

return updatedUser;
} catch (error) {
// check if user not found and throw error
if (error.code === 'P2025') {
throw new NotFoundException(`User with id ${id} not found`);
}

// check if email already registered and throw error
if (error.code === 'P2002') {
throw new ConflictException('Email already registered');
}

// throw error if any
throw new HttpException(error, 500);
}
}

async deleteUser(id: number): Promise<string> {
try {
// find user by id. If not found, throw error
const user = await this.prisma.user.findUniqueOrThrow({
where: { id },
});

// delete user using prisma client
await this.prisma.user.delete({
where: { id },
});

return `User with id ${user.id} deleted`;
} catch (error) {
// check if user not found and throw error
if (error.code === 'P2025') {
throw new NotFoundException(`User with id ${id} not found`);
}

// throw error if any
throw new HttpException(error, 500);
}
}
}

下一步是我们需要更新我们的 to interact with,这是完整的代码:UsersControllerusersService

// src/modules/users/users.controller.ts

import {
Body,
Controller,
Delete,
Get,
Param,
ParseIntPipe,
Patch,
Post,
} from '@nestjs/common';
import { CreateUserDto } from './dtos/create-user.dto';
import { UpdateUsertDto } from './dtos/update-user.dto';
import { LoginUserDto } from './dtos/login-user.dto';
import { UsersService } from './users.service';
import { User } from '@prisma/client';
import { LoginResponse } from './interfaces/users-login.interface';

@Controller('users')
export class UsersController {
// inject users service
constructor(private readonly usersService: UsersService) {}

@Post('register')
async registerUser(@Body() createUserDto: CreateUserDto): Promise<User> {
// call users service method to register new user
return this.usersService.registerUser(createUserDto);
}

@Post('login')
loginUser(@Body() loginUserDto: LoginUserDto): Promise<LoginResponse> {
// call users service method to login user
return this.usersService.loginUser(loginUserDto);
}

@Get('me')
me(): string {
return 'Get my Profile!';
}

@Patch(':id')
async updateUser(
@Param('id', ParseIntPipe) id: number,
@Body() updateUserDto: UpdateUsertDto,
): Promise<User> {
// call users service method to update user
return this.usersService.updateUser(+id, updateUserDto);
}

@Delete(':id')
async deleteUser(@Param('id', ParseIntPipe) id: number): Promise<string> {
// call users service method to delete user
return this.usersService.deleteUser(+id);
}
}

我们尚未更新终端节点。以后再做me

我们快完成了。我们需要注册并:UsersControllerUsersServiceUsersModule

// src/modules/users/users.module.ts

import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({
imports: [],
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}

最后,我们已经完成了 register、login、update 和 delete。现在我们可以测试这些端点:users