构建 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
安装过程中,系统会询问您要使用哪个包管理器(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
软件包安装完毕后,让我们为 GraphQL 服务器创建一个配置文件。
$ touch src/graphql.options.ts
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,
};
}
}
然后注册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 {}
您可能已经注意到我从主模块中移除了 ` AppControllerand` 和AppService`or`。由于我们将使用 GraphQL 而不是 REST API,因此我们不再需要它们。相应的文件也可以删除。
为了测试这个设置,我们来创建一个简单的 GraphQL API schema。
$ mkdir src/schema
$ touch src/schema/gql-api.graphql
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
}
跑步npm start会带来两方面的好处:
- 生成
src/graphql.schema.generated.ts可在源代码中使用的 TypeScript 类型。 - 在端口 3000 上启动服务器。
现在我们可以访问http://localhost:3000/graphql(默认的 GraphQL API 路径)来查看 GraphQL Playground。
棱镜
要运行 Prisma,我们需要安装Docker ,您可以按照这里的安装指南进行操作。
Linux 用户 - 您需要单独安装docker-compose。
我们将运行两个容器——一个用于实际数据库,另一个用于 prisma 服务。
在项目根目录中创建 docker compose 配置文件。
$ touch docker-compose.yml
并将以下配置放在那里。
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: ~
在项目根目录下运行 docker compose 命令。docker compose 将下载镜像并启动容器。
$ docker-compose up -d
Prisma 服务器现在已连接到本地 Postgres 实例,并在 4466 端口上运行。在浏览器中打开http://localhost:4466将打开 Prisma GraphQL playground。
现在我们来安装 Prisma CLI 和 Prisma 客户端辅助库。
$ npm install -g prisma
$ npm install --save prisma-client-lib
在项目根文件夹中初始化 Prisma。
$ prisma init --endpoint http://localhost:4466
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/
然后运行prisma deploy以部署您的服务。它将初始化指定的模式datamodel.prisma并生成 Prisma 客户端。
$ prisma deploy
访问http://localhost:4466/_admin打开 prisma 管理工具,与 graphql playground 相比,这是一种更方便地查看和编辑数据的方式。
棱镜模块
这一步骤其实是可选的,因为你可以直接在其他模块/服务等中使用生成的 Prisma 客户端,但是创建一个 Prisma 模块会更容易在将来进行配置或更改。
让我们使用 NestJS CLI 创建一个 Prisma 模块和一个服务。CLI 会自动创建样板文件,并为我们完成初始模块元数据设置。
$ nest g module prisma
$ nest g service prisma
那么我们来设置一下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();
}
}
并将其导出到src/prisma/prisma.module.ts中。
import { Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}
太好了!初始设置已经完成,现在让我们继续实现身份验证。
示玛斯
数据库架构
让我们把样板应用模式存储在database/datamodel.prisma 文件中。我们还可以删除项目根目录下包含默认模式的旧数据模型文件。
$ rm datamodel.prisma
$ mkdir database
$ touch database/datamodel.prisma
数据库/数据模型.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
}
接下来,我们修改prisma.yml 文件,并定义新模式的路径。
prisma.yml
endpoint: http://localhost:4466
datamodel:
- database/datamodel.prisma
generate:
- generator: typescript-client
output: ./generated/prisma-client/
部署架构后,prisma 客户端将自动更新,您应该在 prisma 管理后台http://localhost:4466/_admin中看到相应的更改。
$ prisma deploy
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!
}
现在启动该应用程序,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
创建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
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;
}
}
身份验证服务的 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);
}
}
在这里,我们定义了令牌的获取方式以及如何验证它。我们将通过环境变量传递 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();
现在让我们创建一个稍后会用到的验证类,并在其中添加一些电子邮件/密码验证。
$ touch src/auth/sign-up-input.dto.ts
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;
}
为了使验证功能正常工作,我们需要从@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 {}
为了方便地从 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,
);
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;
}
}
最后是身份验证逻辑。我们使用它来对 密码和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;
}
}
现在让我们把所有东西都连接起来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 {}
太好了,身份验证已准备就绪!启动服务器,尝试创建用户、登录,并在浏览器中检查 cookie。
如果看到tokencookie,则一切正常。
帖子模块
让我们为应用程序添加一些基本逻辑。获得授权的用户可以
创建所有人都能看到的帖子。
$ nest g module post
$ nest g resolver post
$ touch src/post/post-input.dto.ts
首先,让我们为所有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;
}
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 } },
});
}
}
别忘了在模块中定义所有内容。
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 {}
用户模块
虽然我们没有任何用户变更,但我们仍然需要定义用户解析器,以便 GraphQL 可以正确解析我们的查询。
$ nest g module user
$ nest g resolver user
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();
}
}
当然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 {}
示例查询
您可以运行以下简单查询来测试您的应用程序。
注册
mutation {
signup(signUpInput: { email: "user@email.com", password: "pasword" }) {
id
email
}
}
登录
mutation {
login(loginInput: { email: "user@email.com", password: "pasword" }) {
id
email
}
}
创建帖子
mutation {
createPost(postInput: { title: "Post Title", body: "Post Body" }) {
id
title
author {
id
email
}
}
}
正在检索所有帖子
query {
posts {
title
author {
email
}
}
}
结论
我们的应用样板代码终于完成了!请查看 NestJS 文档,了解如何为你的应用添加更多实用功能。部署到生产环境时,别忘了保护你的 Prisma 层和数据库。
您可以在这里找到最终代码。
文章来源:https://dev.to/nikitakot/building-nestjs-app-boilerplate-authentication-validation-graphql-and-prisma-f1d
