从零开始使用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.ts
await app.listen(ANY_AVAILABLE_PORT_NUMBER);
Nest JS 概述
我们的大部分代码都将写入该文件夹。让我们看一下文件夹。共有 5 个文件:src
src
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 generate
node_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
创建用户终端节点
让我们继续创建终端节点。
在该文件夹中,创建一个名为 的新文件夹。src
modules/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 实施了验证,例如:
现在让我们跳转到 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.create
hash(createUserDto.password, 10)
delete newUser.password
我们还使用 如果存在重复的电子邮件或任何其他错误 来处理错误。throw new ConflictException
throw new HttpException
现在让我们继续登录函数:
登录响应将返回一个具有名为 () 的单个属性的对象,其中包含一些使用 生成的用户数据 (),因此我们需要先安装:access_token
interface LoginResponse
interface UserPaylod
jwt
@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);
}
}
到目前为止,我们已经创建了两个函数,和 .现在让我们继续 和 。registerUser
loginUser
updateUser
deleteUser
这是函数: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
POST /users/register
,POST /users/login
,PATCH /users/:id
,DELETE /users/:id
- 到目前为止,我们一直做得很好 🙂
- 原文来源:https://dev.to/alfism1/build-complete-rest-api-feature-with-nest-js-using-prisma-and-postgresql-from-scratch-beginner-friendly-part-1-5b73