发布于 2026-01-05 5 阅读
0

构建 NestJS 应用样板 - 身份验证、验证、GraphQL 和 Prisma ⚠️⚠️⚠️ 更新 - 2020 年 4 月 6 日 简介 入门指南 架构模块 示例查询 结论

构建 NestJS 应用样板 - 身份验证、验证、GraphQL 和 Prisma

⚠️⚠️⚠️ 更新 - 2020年4月6日

引言

入门

示玛斯

模块

示例查询

结论

本教程创建的样板应用程序在这里

⚠️⚠️⚠️ 更新 - 2020年4月6日

NestJS 7 版本已于近期发布。非常感谢
johnbiundo发布此次版本更新所需的更改说明。GitHub 代码库也已更新,您可以在这里查看我所做的更改。

引言

NestJS是 Node 世界中一个相对较新的框架。它受 Angular 启发,基于 Express 构建,并完全支持 TypeScript,为您的应用程序提供可扩展且易于维护的架构。NestJS 还支持GraphQL——一种强大的 API 查询语言,并提供了一个专用的、即用@nestjs/graphql型模块(实际上,该模块只是 Apollo 服务器的一个封装)。

在本教程中,我们将构建一个包含所有基本功能的样板应用程序,这些功能将帮助您开发更复杂的应用程序。我们将使用Prisma作为数据库层,因为它与 GraphQL API 的兼容性极佳,可以轻松地将 Prisma 解析器映射到 GraphQL API 解析器。

本文结束时,我们将创建一个简单的博客应用程序,允许用户注册、登录和创建帖子。

入门

NestJS

要开始使用 NestJS,您需要安装 Node.js(版本 >= 8.9.0)和 npm。您可以从官方网站下载并安装Node.js。

安装好 node 和 npm 之后,我们来安装 NestJS CLI 并初始化一个新项目。

$ npm i -g @nestjs/cli
$ nest new nestjs-boilerplate
$ cd nestjs-boilerplate
Enter fullscreen mode Exit fullscreen mode

安装过程中,系统会询问您要使用哪个包管理器(yarn 或 npm)。本教程将使用 npm,但如果您更喜欢 yarn,也可以使用。

现在让我们运行它npm start。它将在端口 3000 上启动应用程序,因此在浏览器中打开http://localhost:3000将显示“Hello World!”消息。

GraphQL

如上所述,我们将使用@nestjs/graphql模块为我们的 API 设置 GraphQL。

$ npm i --save @nestjs/graphql apollo-server-express graphql-tools graphql
Enter fullscreen mode Exit fullscreen mode

软件包安装完毕后,让我们为 GraphQL 服务器创建一个配置文件。

$ touch src/graphql.options.ts
Enter fullscreen mode Exit fullscreen mode

NestJS会将配置信息传递给底层的Apollo实例。更详细的文档请参见此处

src/graphql.options.ts

import { GqlModuleOptions, GqlOptionsFactory } from '@nestjs/graphql';
import { Injectable } from '@nestjs/common';
import { join } from 'path';

@Injectable()
export class GraphqlOptions implements GqlOptionsFactory {
  createGqlOptions(): Promise<GqlModuleOptions> | GqlModuleOptions {
    return {
      context: ({ req, res }) => ({ req, res }),
      typePaths: ['./src/*/*.graphql'], // path for gql schema files
      installSubscriptionHandlers: true,
      resolverValidationOptions: {
        requireResolversForResolveType: false,
      },
      definitions: { // will generate .ts types from gql schema files
        path: join(process.cwd(), 'src/graphql.schema.generated.ts'),
        outputAs: 'class',
      },
      debug: true,
      introspection: true,
      playground: true,
      cors: false,
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

然后注册GraphQLModule并将配置信息传递到应用程序的主AppModule模块中。

src/app.module.ts

import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { GraphqlOptions } from './graphql.options';

@Module({
  imports: [
    GraphQLModule.forRootAsync({
      useClass: GraphqlOptions,
    }),
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

您可能已经注意到我从主模块中移除了 ` AppControllerand` 和AppService`or`。由于我们将使用 GraphQL 而不是 REST API,因此我们不再需要它们。相应的文件也可以删除。

为了测试这个设置,我们来创建一个简单的 GraphQL API schema。

$ mkdir src/schema 
$ touch src/schema/gql-api.graphql
Enter fullscreen mode Exit fullscreen mode

src/schema/gql-api.graphql

type Author {
    id: Int!
    firstName: String
    lastName: String
    posts: [Post]
}

type Post {
    id: Int!
    title: String!
    votes: Int
}

type Query {
    author(id: Int!): Author
}
Enter fullscreen mode Exit fullscreen mode

跑步npm start会带来两方面的好处:

  • 生成src/graphql.schema.generated.ts可在源代码中使用的 TypeScript 类型。
  • 在端口 3000 上启动服务器。

现在我们可以访问http://localhost:3000/graphql(默认的 GraphQL API 路径)来查看 GraphQL Playground。

GraphQL Playground

棱镜

要运行 Prisma,我们需要安装Docker ,您可以按照这里的安装指南进行操作

Linux 用户 - 您需要单独安装docker-compose

我们将运行两个容器——一个用于实际数据库,另一个用于 prisma 服务。

在项目根目录中创建 docker compose 配置文件。

$ touch docker-compose.yml
Enter fullscreen mode Exit fullscreen mode

并将以下配置放在那里。

docker-compose.yml

version: '3'
services:
  prisma:
    image: prismagraphql/prisma:1.34
    ports:
      - '4466:4466'
    environment:
      PRISMA_CONFIG: |
        port: 4466
        databases:
          default:
            connector: postgres
            host: postgres
            port: 5432
            user: prisma
            password: prisma
  postgres:
    image: postgres:10.3
    environment:
      POSTGRES_USER: prisma
      POSTGRES_PASSWORD: prisma
    volumes:
      - postgres:/var/lib/postgresql/data
volumes:
  postgres: ~
Enter fullscreen mode Exit fullscreen mode

在项目根目录下运行 docker compose 命令。docker compose 将下载镜像并启动容器。

$ docker-compose up -d
Enter fullscreen mode Exit fullscreen mode

Prisma 服务器现在已连接到本地 Postgres 实例,并在 4466 端口上运行。在浏览器中打开http://localhost:4466将打开 Prisma GraphQL playground。

现在我们来安装 Prisma CLI 和 Prisma 客户端辅助库。

$ npm install -g prisma 
$ npm install --save prisma-client-lib
Enter fullscreen mode Exit fullscreen mode

在项目根文件夹中初始化 Prisma。

$ prisma init --endpoint http://localhost:4466
Enter fullscreen mode Exit fullscreen mode

Prisma 初始化会在项目根目录创建 `.prisma.conf`datamodel.prisma和 `.prisma.conf`文件。`.prisma.conf`文件包含数据库模式,`.prisma.conf` 文件包含 Prisma 客户端配置。prisma.ymldatamodel.prismaprisma.yml

添加以下代码以prisma.yml生成typescript-client查询数据库所需的代码。

prisma.yml

endpoint: http://localhost:4466
datamodel: datamodel.prisma
generate:
  - generator: typescript-client
    output: ./generated/prisma-client/
Enter fullscreen mode Exit fullscreen mode

然后运行prisma deploy以部署您的服务。它将初始化指定的模式datamodel.prisma并生成 Prisma 客户端。

$ prisma deploy
Enter fullscreen mode Exit fullscreen mode

访问http://localhost:4466/_admin打开 prisma 管理工具,与 graphql playground 相比,这是一种更方便地查看和编辑数据的方式。

棱镜模块

这一步骤其实是可选的,因为你可以直接在其他模块/服务等中使用生成的 Prisma 客户端,但是创建一个 Prisma 模块会更容易在将来进行配置或更改。

让我们使用 NestJS CLI 创建一个 Prisma 模块和一个服务。CLI 会自动创建样板文件,并为我们完成初始模块元数据设置。

$ nest g module prisma 
$ nest g service prisma
Enter fullscreen mode Exit fullscreen mode

那么我们来设置一下PrismaService

src/prisma/prisma.service.ts

import { Injectable } from '@nestjs/common';
import { Prisma } from '../../generated/prisma-client';

@Injectable()
export class PrismaService {
  client: Prisma;

  constructor() {
    this.client = new Prisma();
  }
}
Enter fullscreen mode Exit fullscreen mode

并将其导出到src/prisma/prisma.module.ts中。

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

@Module({
  providers: [PrismaService],
  exports: [PrismaService],
})
export class PrismaModule {}
Enter fullscreen mode Exit fullscreen mode

太好了!初始设置已经完成,现在让我们继续实现身份验证。

示玛斯

数据库架构

让我们把样板应用模式存储在database/datamodel.prisma 文件中。我们还可以删除项目根目录下包含默认模式的旧数据模型文件。

$ rm datamodel.prisma
$ mkdir database
$ touch database/datamodel.prisma
Enter fullscreen mode Exit fullscreen mode

数据库/数据模型.prisma

type User {
    id: ID! @id
    email: String! @unique
    password: String!
    post: [Post!]!
    createdAt: DateTime! @createdAt
    updatedAt: DateTime! @updatedAt
}

type Post {
    id: ID! @id
    title: String!
    body: String
    author: User!
    createdAt: DateTime! @createdAt
    updatedAt: DateTime! @updatedAt
}
Enter fullscreen mode Exit fullscreen mode

接下来,我们修改prisma.yml 文件,并定义新模式的路径。

prisma.yml

endpoint: http://localhost:4466
datamodel:
  - database/datamodel.prisma
generate:
  - generator: typescript-client
    output: ./generated/prisma-client/
Enter fullscreen mode Exit fullscreen mode

部署架构后,prisma 客户端将自动更新,您应该在 prisma 管理后台http://localhost:4466/_admin中看到相应的更改。

$ prisma deploy
Enter fullscreen mode Exit fullscreen mode

API架构

让我们把以下 GraphQL API 模式放在src/schema/gql-api.graphql中。

src/schema/gql-api.graphql

type User {
  id: ID!
  email: String!
  post: [Post!]!
  createdAt: String!
  updatedAt: String!
}

type Post {
  id: ID!
  title: String!
  body: String
  author: User!
}

input SignUpInput {
  email: String!
  password: String!
}

input LoginInput {
  email: String!
  password: String!
}

input PostInput {
  title: String!
  body: String
}

type AuthPayload {
  id: ID!
  email: String!
}

type Query {
  post(id: ID!): Post!
  posts: [Post!]!
}

type Mutation {
  signup(signUpInput: SignUpInput): AuthPayload!
  login(loginInput: LoginInput): AuthPayload!
  createPost(postInput: PostInput): Post!
}
Enter fullscreen mode Exit fullscreen mode

现在启动该应用程序,npm start以便它根据上面的模式生成 TypeScript 类型。

模块

身份验证模块

首先,我们需要安装一些额外的软件包,以便在我们的 NestJS 应用程序中实现 passport JWT。

$ npm install --save @nestjs/passport passport @nestjs/jwt passport-jwt cookie-parser bcryptjs class-validator class-transformer
$ npm install @types/passport-jwt --save-dev
Enter fullscreen mode Exit fullscreen mode

创建AuthModule、、文件AuthServiceAuthResolverJwtStrategyGqlAuthGuard

$ nest g module auth 
$ nest g service auth
$ nest g resolver auth
$ touch src/auth/jwt.strategy.ts
$ touch src/auth/graphql-auth.guard.ts 
Enter fullscreen mode Exit fullscreen mode

src/auth/auth.service.ts

import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { User } from '../../generated/prisma-client';

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

  async validate({ id }): Promise<User> {
    const user = await this.prisma.client.user({ id });
    if (!user) {
      throw Error('Authenticate validation error');
    }
    return user;
  }
}
Enter fullscreen mode Exit fullscreen mode

身份验证服务的 validate 方法将检查 JWT 令牌中的用户 ID 是否已持久化到数据库中。

src/auth/jwt.strategy.ts

import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-jwt';
import { Request } from 'express';
import { AuthService } from './auth.service';

const cookieExtractor = (req: Request): string | null => {
  let token = null;
  if (req && req.cookies) {
    token = req.cookies.token;
  }
  return token;
};

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(private readonly authService: AuthService) {
    super({
      jwtFromRequest: cookieExtractor,
      secretOrKey: process.env.JWT_SECRET,
    });
  }

  validate(payload) {
    return this.authService.validate(payload);
  }
}
Enter fullscreen mode Exit fullscreen mode

在这里,我们定义了令牌的获取方式以及如何验证它。我们将通过环境变量传递 JWT 密钥,因此您将使用以下命令启动应用程序JWT_SECRET=your_secret_here npm run start

为了能够解析 cookie,我们需要定义全局cookie-parser中间件。

src/main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as cookieParser from 'cookie-parser';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.use(cookieParser());
  await app.listen(3000);
}
bootstrap();
Enter fullscreen mode Exit fullscreen mode

现在让我们创建一个稍后会用到的验证类,并在其中添加一些电子邮件/密码验证。

$ touch src/auth/sign-up-input.dto.ts
Enter fullscreen mode Exit fullscreen mode

src/auth/sign-up-input.dto.ts

import { IsEmail, MinLength } from 'class-validator';
import { SignUpInput } from '../graphql.schema.generated';

export class SignUpInputDto extends SignUpInput {
  @IsEmail()
  readonly email: string;

  @MinLength(6)
  readonly password: string;
}
Enter fullscreen mode Exit fullscreen mode

为了使验证功能正常工作,我们需要从@nestjs/common包中全局定义验证管道。

src/app.module.ts

import { Module, ValidationPipe } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { GraphqlOptions } from './graphql.options';
import { PrismaModule } from './prisma/prisma.module';
import { AuthModule } from './auth/auth.module';
import { APP_PIPE } from '@nestjs/core';

@Module({
  imports: [
    GraphQLModule.forRootAsync({
      useClass: GraphqlOptions,
    }),
    PrismaModule,
    AuthModule,
  ],
  providers: [
    {
      provide: APP_PIPE,
      useClass: ValidationPipe,
    },
  ],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

为了方便地从 GraphQL 上下文中访问请求对象和用户对象,我们可以创建装饰器。有关自定义装饰器的更多信息,请点击此处

src/shared/decorators/decorators.ts

import { createParamDecorator } from '@nestjs/common';
import { Response } from 'express';
import { User } from '../../../generated/prisma-client';

export const ResGql = createParamDecorator(
  (data, [root, args, ctx, info]): Response => ctx.res,
);

export const GqlUser = createParamDecorator(
  (data, [root, args, ctx, info]): User => ctx.req && ctx.req.user,
);
Enter fullscreen mode Exit fullscreen mode

src/auth/auth.resolver.ts

import * as bcryptjs from 'bcryptjs';
import { Response } from 'express';
import { Args, Mutation, Resolver } from '@nestjs/graphql';
import { LoginInput } from '../graphql.schema.generated';
import { ResGql } from '../shared/decorators/decorators';
import { JwtService } from '@nestjs/jwt';
import { PrismaService } from '../prisma/prisma.service';
import { SignUpInputDto } from './sign-up-input.dto';

@Resolver('Auth')
export class AuthResolver {
  constructor(
    private readonly jwt: JwtService,
    private readonly prisma: PrismaService,
  ) {}

  @Mutation()
  async login(
    @Args('loginInput') { email, password }: LoginInput,
    @ResGql() res: Response,
  ) {
    const user = await this.prisma.client.user({ email });
    if (!user) {
      throw Error('Email or password incorrect');
    }

    const valid = await bcryptjs.compare(password, user.password);
    if (!valid) {
      throw Error('Email or password incorrect');
    }

    const jwt = this.jwt.sign({ id: user.id });
    res.cookie('token', jwt, { httpOnly: true });

    return user;
  }

  @Mutation()
  async signup(
    @Args('signUpInput') signUpInputDto: SignUpInputDto,
    @ResGql() res: Response,
  ) {
    const emailExists = await this.prisma.client.$exists.user({
      email: signUpInputDto.email,
    });
    if (emailExists) {
      throw Error('Email is already in use');
    }
    const password = await bcryptjs.hash(signUpInputDto.password, 10);

    const user = await this.prisma.client.createUser({ ...signUpInputDto, password });

    const jwt = this.jwt.sign({ id: user.id });
    res.cookie('token', jwt, { httpOnly: true });

    return user;
  }
}
Enter fullscreen mode Exit fullscreen mode

最后是身份验证逻辑。我们使用它来对 密码和cookie 进行bcryptjs哈希处理和安全保护,以防止 客户端遭受 XSS 攻击。
httpOnly

如果我们想让某些端点仅对已注册用户开放,我们需要
创建一个身份验证守卫,然后将其用作端点
定义上方的装饰器。

src/auth/graphql-auth.guard.ts

import { ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GqlExecutionContext } from '@nestjs/graphql';

@Injectable()
export class GqlAuthGuard extends AuthGuard('jwt') {
  getRequest(context: ExecutionContext) {
    const ctx = GqlExecutionContext.create(context);
    return ctx.getContext().req;
  }
}
Enter fullscreen mode Exit fullscreen mode

现在让我们把所有东西都连接起来AuthModule

import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthResolver } from './auth.resolver';
import { PrismaModule } from '../prisma/prisma.module';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { JwtStrategy } from './jwt.strategy';

@Module({
  imports: [
    PrismaModule,
    PassportModule.register({
      defaultStrategy: 'jwt',
    }),
    JwtModule.register({
      secret: process.env.JWT_SECRET,
      signOptions: {
        expiresIn: 3600, // 1 hour
      },
    }),
  ],
  providers: [AuthService, AuthResolver, JwtStrategy],
})
export class AuthModule {}
Enter fullscreen mode Exit fullscreen mode

太好了,身份验证已准备就绪!启动服务器,尝试创建用户、登录,并在浏览器中检查 cookie。
如果看到tokencookie,则一切正常。

帖子模块

让我们为应用程序添加一些基本逻辑。获得授权的用户可以
创建所有人都能看到的帖子。

$ nest g module post
$ nest g resolver post
$ touch src/post/post-input.dto.ts
Enter fullscreen mode Exit fullscreen mode

首先,让我们为所有Post字段定义解析器,并为变更添加简单的验证createPost

src/post/post-input.dto.ts

import { IsString, MaxLength, MinLength } from 'class-validator';
import { PostInput } from '../graphql.schema.generated';

export class PostInputDto extends PostInput {
  @IsString()
  @MinLength(10)
  @MaxLength(60)
  readonly title: string;
}
Enter fullscreen mode Exit fullscreen mode

src/post/post.resolver.ts

import {
  Args,
  Mutation,
  Parent,
  Query,
  ResolveProperty,
  Resolver,
} from '@nestjs/graphql';
import { PrismaService } from '../prisma/prisma.service';
import { Post } from '../graphql.schema.generated';
import { GqlUser } from '../shared/decorators/decorators';
import { User } from '../../generated/prisma-client';
import { UseGuards } from '@nestjs/common';
import { GqlAuthGuard } from '../auth/graphql-auth.guard';
import { PostInputDto } from './post-input.dto';

@Resolver('Post')
export class PostResolver {
  constructor(private readonly prisma: PrismaService) {}

  @Query()
  async post(@Args('id') id: string) {
    return this.prisma.client.post({ id });
  }

  @Query()
  async posts() {
    return this.prisma.client.posts();
  }

  @ResolveProperty()
  async author(@Parent() { id }: Post) {
    return this.prisma.client.post({ id }).author();
  }

  @Mutation()
  @UseGuards(GqlAuthGuard)
  async createPost(
    @Args('postInput') { title, body }: PostInputDto,
    @GqlUser() user: User,
  ) {
    return this.prisma.client.createPost({
      title,
      body,
      author: { connect: { id: user.id } },
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

别忘了在模块中定义所有内容。

src/post/post.module.ts

import { Module } from '@nestjs/common';
import { PostResolver } from './post.resolver';
import { PrismaModule } from '../prisma/prisma.module';

@Module({
  providers: [PostResolver],
  imports: [PrismaModule],
})
export class PostModule {}
Enter fullscreen mode Exit fullscreen mode

用户模块

虽然我们没有任何用户变更,但我们仍然需要定义用户解析器,以便 GraphQL 可以正确解析我们的查询。

$ nest g module user 
$ nest g resolver user
Enter fullscreen mode Exit fullscreen mode

src/user/user.resolver.ts

import { Parent, ResolveProperty, Resolver } from '@nestjs/graphql';
import { PrismaService } from '../prisma/prisma.service';
import { User } from '../graphql.schema.generated';

@Resolver('User')
export class UserResolver {
  constructor(private readonly prisma: PrismaService) {}

  @ResolveProperty()
  async post(@Parent() { id }: User) {
    return this.prisma.client.user({ id }).post();
  }
}
Enter fullscreen mode Exit fullscreen mode

当然UserModule

src/user/user.module.ts

import { Module } from '@nestjs/common';
import { UserResolver } from './user.resolver';
import { PrismaModule } from '../prisma/prisma.module';

@Module({
  providers: [UserResolver],
  imports: [PrismaModule],
})
export class UserModule {}
Enter fullscreen mode Exit fullscreen mode

示例查询

您可以运行以下简单查询来测试您的应用程序。

注册

mutation {
  signup(signUpInput: { email: "user@email.com", password: "pasword" }) {
    id
    email
  }
}
Enter fullscreen mode Exit fullscreen mode

登录

mutation {
  login(loginInput: { email: "user@email.com", password: "pasword" }) {
    id
    email
  }
}
Enter fullscreen mode Exit fullscreen mode

创建帖子

mutation {
  createPost(postInput: { title: "Post Title", body: "Post Body" }) {
    id
    title
    author {
      id
      email
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

正在检索所有帖子

query {
  posts {
    title
    author {
      email
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

结论

我们的应用样板代码终于完成了!请查看 NestJS 文档,了解如何为你的应用添加更多实用功能。部署到生产环境时,别忘了保护你的 Prisma 层和数据库。

您可以在这里找到最终代码

文章来源:https://dev.to/nikitakot/building-nestjs-app-boilerplate-authentication-validation-graphql-and-prisma-f1d