掘金 后端 ( ) • 2024-04-14 17:56

本文将带领你深入探究如何利用 Nest.js 和 Prisma 这两个强大的工具来构建优秀的后端应用。我们将从安装与配置开始,逐步探索如何构建 RESTful API、管理数据模型、实现身份验证与授权、进行错误处理与日志记录等方面的内容。

1. 环境安装与配置

1. 安装 Node.js 和 npm

确保你的计算机上已经安装了 Node.js 和 npm(Node.js 包管理器)。你可以在 Node.js 官网 上下载并安装最新版本的 Node.js。

这里更推荐使用 pnpm,它可以减少磁盘空间的使用,并且在安装依赖时更快。

2. 创建 Nest.js 项目

根据官方文档创建模版

npm i -g @nestjs/cli
nest new nest-prisma-example

3. 安装 Prisma 相关依赖

cd nest-prisma-example
pnpm i prisma
pnpm i -D @prisma/client

4. 配置 Prisma

VSCode 推荐大家安装 prsima 插件

npx prisma init

命令会在项目根目录生成 prisma/schema.prisma 文件。

打开 schema.prisma 文件,修改 datasource db 数据库配置,我使用的是 PostgreSQL,使用其他数据库修改 provider 即可。

datasource db {
  provider = "postgresql"
  url      = "postgresql://postgres:postgres@localhost:5432/db_example"
}

多环境情况下可以采用 env("DATABASE_URL") 来使用,参考文档

2. 数据模型定义

1. 定义数据模型

编辑 schema.prisma 文件,使用 Prisma Schema Language 定义你的数据模型。在模型中定义实体、字段、关联等。例如:


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

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

2. 使用 Prisma Client

通过运行以下命令来生成 Prisma Client:

npx prisma generate

这将在你的项目中生成 Prisma Client,你可以使用它来与数据库交互,执行 CRUD 操作等。

通过运行以下命令来生成数据库表结构:

prisma db push --preview-feature

3. 配置 tsconfig.json

{
    "paths": {
      "@prisma/client": ["./prisma/client"]
    }
}

4. 在 Nest.js 中使用 Prisma

src/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();
  }
}

service 中使用它来执行数据库操作。

import { PrismaService } from './prisma.service';

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

  async findAll(): Promise<User[]> {
    return this.prisma.user.findMany();
  }

  async findById(id: number): Promise<User> {
    return this.prisma.user.findUnique({
      where: { id },
    });
  }

  async createUser(data: CreateUserDto): Promise<User> {
    return this.prisma.user.create({
      data,
    });
  }

  async updateUser(id: number, data: UpdateUserDto): Promise<User> {
    return this.prisma.user.update({
      where: { id },
      data,
    });
  }

  async deleteUser(id: number): Promise<User> {
    return this.prisma.user.delete({
      where: { id },
    });
  }
}

3. 构建 RESTful API

1. 创建控制器(Controller)

首先,你需要在 src/controllers 创建一个控制器来处理 API 请求。控制器是负责接收 HTTP 请求并返回响应的地方。你可以使用 Nest CLI 来创建一个新的控制器:

nest generate controller your-controller-name

这将在你的项目中创建一个新的控制器文件,并为你生成基本的控制器代码结构。

2. 定义路由(Routes)

在控制器中,使用装饰器 @Get@Post@Put@Delete 等来定义路由,以处理对应的 HTTP 请求。例如:

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

@Controller('user')
export class CatsController {
  @Get()
  findAll(): string {
    return 'This action returns all cats';
  }
}

在上面的例子中,@Controller('user') 表示该控制器处理的请求路径为 /user@Get() 表示该路由处理 GET 请求,并指定了 findAll() 方法来处理该路由。

3. 编写服务(Service)

控制器通常不处理具体的业务逻辑,而是将请求委托给服务来处理。服务是用于处理业务逻辑的地方,包括数据的获取、处理和返回。在 src/services 你可以使用 Nest CLI 来创建一个新的服务:

nest generate service your-service-name

然后,在控制器中使用 import 语句导入你的服务,并在控制器中调用服务的方法来处理请求。

4. 注册模块(Module)

最后,将控制器和服务注册到相应的模块中。Nest.js 使用模块来组织应用程序的代码,因此你需要在模块中将控制器和服务关联起来。可以通过 @Module() 装饰器来定义一个模块,使用 imports 字段来导入依赖的模块,使用 controllers 字段来注册控制器,使用 providers 字段来注册服务。

// src/app.module.ts
import { Module } from '@nestjs/common';
import { UserController } from './controllers/user/user.controller';
import { UserService } from './services//user/user.service';

@Module({
  controllers: [UserController],
  providers: [UserService],
})
export class AppModule {}

5. 启动应用程序

启动你的 Nest.js 应用程序,你可以使用 nest start 或者 pnpm start,然后访问你定义的路由路径来测试你的 RESTful API。

4. 身份验证与授权

1. 安装相关库

pnpm install @nestjs/passport passport passport-local passport-jwt jsonwebtoken

2. 创建本地身份验证策略

// local.strategy.ts
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private authService: AuthService) {
    super();
  }

  async validate(username: string, password: string): Promise<any> {
    const user = await this.authService.validateUser(username, password);
    if (!user) {
      throw new UnauthorizedException();
    }
    return user;
  }
}

3. 创建 JWT 身份验证策略

// jwt.strategy.ts
import { Strategy, ExtractJwt } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';
import { JwtPayload } from './jwt-payload.interface';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(private authService: AuthService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: 'your-secret-key',
    });
  }

  async validate(payload: JwtPayload): Promise<any> {
    const user = await this.authService.validateUserById(payload.sub);
    if (!user) {
      throw new UnauthorizedException();
    }
    return user;
  }
}

4. 创建 AuthService

// auth.service.ts
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../users/users.service';

@Injectable()
export class AuthService {
  constructor(
    private usersService: UsersService,
    private jwtService: JwtService,
  ) {}

  async validateUser(username: string, password: string): Promise<any> {
    const user = await this.usersService.findByUsername(username);
    if (user && user.password === password) {
      return user;
    }
    return null;
  }

  async validateUserById(id: number): Promise<any> {
    return this.usersService.findById(id);
  }

  async login(user: any): Promise<any> {
    const payload = { username: user.username, sub: user.id };
    return {
      access_token: this.jwtService.sign(payload),
    };
  }
}

5. 创建 AuthModule

// auth.module.ts
import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { AuthService } from './auth.service';
import { LocalStrategy } from './local.strategy';
import { JwtStrategy } from './jwt.strategy';

@Module({
  imports: [
    PassportModule,
    JwtModule.register({
      secret: 'your-secret-key',
      signOptions: { expiresIn: '1h' },
    }),
  ],
  providers: [AuthService, LocalStrategy, JwtStrategy],
  exports: [AuthService],
})
export class AuthModule {}

6. 使用 AuthModule

定义 JwtAuthGuard


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

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

controller 文件中使用 @UseGuards() 装饰器来应用认证守卫。

import { Controller, Get, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from './jwt-auth.guard'; // 导入你的 JWT 认证守卫

@Controller('user')
export class CatsController {
  constructor() {}

  @Get()
  @UseGuards(JwtAuthGuard) // 应用 JWT 认证守卫
  findAll(): string {
    return 'This action returns all users';
  }
}

5. 错误处理与日志记录

在 Nest.js 中,你可以使用拦截器(Interceptors)来处理全局的错误和日志记录,以及使用异常过滤器(Exception Filters)来处理特定类型的异常。

1. 错误处理

1. 全局错误处理

你可以创建一个全局的拦截器来处理全局的错误和日志记录。这个拦截器会捕获所有的异常并进行处理。

import { Injectable, NestInterceptor, ExecutionContext, CallHandler, BadGatewayException, Logger } from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';

@Injectable()
export class ErrorsInterceptor implements NestInterceptor {
  private logger = new Logger('ErrorsInterceptor');

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      catchError(error => {
        this.logger.error(`Error occurred: ${error}`);
        return throwError(new BadGatewayException());
      }),
    );
  }
}

然后,将这个拦截器应用到你的应用程序中:

import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { ErrorsInterceptor } from './errors.interceptor';

@Module({
  providers: [
    {
      provide: APP_INTERCEPTOR,
      useClass: ErrorsInterceptor,
    },
  ],
})
export class AppModule {}

2. 异常过滤器

如果你想对特定类型的异常进行处理,可以使用异常过滤器。例如,你可以针对 HttpException 进行特定的处理。

import { Catch, ExceptionFilter, HttpException, ArgumentsHost, Logger } from '@nestjs/common';
import { Request, Response } from 'express';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  private logger = new Logger('HttpExceptionFilter');

  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();

    this.logger.error(`HTTP Exception: ${exception.message}`, exception.stack);

    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
    });
  }
}

然后,在你的应用程序中注册这个过滤器:

import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { HttpExceptionFilter } from './http-exception.filter';

@Module({
  providers: [
    {
      provide: APP_FILTER,
      useClass: HttpExceptionFilter,
    },
  ],
})
export class AppModule {}

2. 日志记录

你可以使用 Nest.js 内置的 Logger 来记录日志。

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

@Injectable()
export class CatsService {
  private logger = new Logger('CatsService');

  findAll(): string[] {
    this.logger.log('Finding all cats...');
    return ['Cat1', 'Cat2', 'Cat3'];
  }
}

Logger 支持几个不同的日志级别,包括 log()error()warn()debug()verbose()。你可以根据需要选择适当的日志级别来记录不同类型的信息。

你也可以将日志记录到文件或其他目标,具体取决于你的需求和配置。

6. 小结

综上所述,Nest.js 和 Prisma 的结合为后端开发提供了一个现代化、高效且可维护的解决方案,使得开发者能够快速构建出功能丰富、高性能的应用程序,并且更容易地适应不断变化的业务需求。
现在的 Node.js 可不是以前的 "不适合大型项目" 的 Node.js了。