在NestJS中构建GraphQL服务器

发布于1/19/2020 来自:「前端知否」微信公众号

NestJS通过为Node.js提供开箱即用的适当模块化和可测试结构,引入了一种现代,时尚的方式来使用Node.js构建后端应用程序。 默认情况下,它还提供TypeScript和依赖项注入支持,这大大提高了我们项目中的代码质量。

今天,我们将学习使用NestJS框架构建GraphQL服务器,通过使用MongoDB构建简单的数据库CRUD应用程序。

此外,您还将学习GraphQL的基础知识以及NestJS开发的一些最佳实践。

那么,在不浪费更多时间的情况下,让我们开始吧。

准备工作

在学习之前,我建议您掌握这些技术的基础知识,并安装最新的工具:

  • 有关使用JavaScript构建应用程序的技术知识以及TypeScript的基础知识。
  • NestJS及其构建基块的基本知识,这个可以在本文中找到
  • 安装在本地系统上的Node.js和MongoDB
  • GraphQL基础

GraphQL基础

GraphQL是一种查询语言和运行时,可用于构建API,并且暴露为强类型模式,而不是容易凌乱的REST接口。用户可以看到数据组织和结构,并且可以查询他们特别想要的字段。

下边是您需要了解的关键概念:

  • 模式-GraphQL服务器实现的核心。描述客户端应用程序可用的功能
  • 查询-请求读取或获取值
  • 变化-修改数据存储中数据的查询
  • 类型—定义在GraphQL中使用的数据的结构
  • 解析器-生成针对GraphQL查询的响应的函数集合

NestJS为我们提供了两种不同的构建GraphQL应用程序的方式,分别是架构优先代码优先

  • 模式优先-在模式优先方法中,基础是GraphQL SDL(模式定义语言),并且GraphQL模式的TypeScript定义将由NestJS自动生成
  • 代码优先—在代码优先方法中,您只需要在TypeScript类中使用装饰器来生成相应的GraphQL模式

在本文中,我选择了“代码优先”方法,个人认为对于GraphQL经验很少或没有GraphQL经验的人更容易理解和遵循本教程。

如果您想进一步了解GraphQL及其概念,我强烈建议您查看官方文档

安装NestJS及其依赖项

现在我们知道了要构建的内容以及为什么要使用每种特定的工具和技术,让我们开始创建项目并安装所需的依赖项。

首先,我们安装Nest CLI并使用它来创建项目

npm i -g @nestjs/cli
nest new nest-graphql

之后,让我们进入目录并安装所需的依赖项

cd nest-graphql
npm i --save @nestjs/graphql apollo-server-express graphql-tools graphql @nestjs/mongoose mongoose type-graphql

现在,我们已经安装了所有内容,并完成了基本项目的设置,让我们创建要使用的文件。

首先,使用NestJS CLI创建模块,服务和控制器

nest g module items
nest g service items
nest g resolver items

之后,手动创建文件和文件夹,项目结构如下所示:

xxx

启动服务器

完成设置过程后,您现在可以使用以下方法启动服务器:

npm run start

这将在默认端口3000上启动应用程序。现在,您只需要在浏览器中访问http:// localhost:3000,会看到类似以下内容:

xxx

添加GraphQL

现在,我们已经完成了基本的服务器设置,让我们继续将GraphQL依赖项导入到我们的应用程序模块中:

import { GraphQLModule } from '@nestjs/graphql';

@Module({
imports: [
GraphQLModule.forRoot({
autoSchemaFile: 'schema.gql',
})
]
})
export class AppModule {}

在这里,我们从上面安装的@nest/ raphql导入GraphQLModule。 然后,我们使用forRoot()方法在imports语句中使用它,该方法将选项对象作为参数。

在选项对象中,我们指定启动服务器时创建的自动生成的GraphQL文件的名称。 该文件是我们上面讨论的代码优先方法的一部分。

连接MongoDB

接下来,将Mongoose模块导入到我们的ApplicationModule中,创建应用程序数据库的连接。

import { MongooseModule } from '@nestjs/mongoose';

@Module({
imports: [
MongooseModule.forRoot('mongodb://localhost/nestgraphql')
],
})
export class AppModule {}

在这里,我们使用forRoot()函数,该函数接收一个配置对象,和我们通常使用的mongoose.connect()函数相同的配置对象。

数据库架构(shema)

数据模式用于正确构造存储在数据库中的数据类型:

src/items/item.schema.ts:

import * as mongoose from 'mongoose';

export const ItemSchema = new mongoose.Schema({
title: String,
price: Number,
description: String,
});

在这里,我们通过导入mongoose,并使用mongoose.Schema来创建新对象来定义schema。

接口

接下来,我们将创建一个TypeScript接口,该接口将用于我们的服务和接收器中的类型检查。

src/items/interfaces/item.interface.ts:

import { Document } from' mongoose';

export interface Item extends Document {
readonly title: string;
readonly price: number;
readonly description: string;
}

DTO

DTO(数据传输对象)是一个通过网络发送数据的对象,也就是需要通过网络传输的数据。

src/items/dto/create-item.dto.ts:

export class ItemType {
readonly id: string;
readonly title: string;
readonly price: number;
readonly description: string;
}

src/items/input-items.input.ts:

import { InputType, Field, Int } from 'type-graphql';

@InputType()
export class ItemInput {
@Field()
readonly title: string;

@Field(() => Int)
readonly price: number;

@Field()
readonly description: string;
}

导入数据库表schema

现在,我们已经为数据库创建了所有需要的文件,我们只需要将schema导入到ItemsModule中即可。

import { Module } from '@nestjs/common';
import { ItemsResolver } from'./items.resolver';
import { ItemSchema } from'./item.schema';
import { MongooseModule } from'@nestjs/mongoose';
import { ItemsService } from'./items.service';

@Module({
imports: [
MongooseModule.forFeature([{name: 'Item',schema: ItemSchema}])
],
providers: [
ItemsResolver,ItemsService
],
})
export class ItemsModule {}

在这里,我们像在ApplicationModule中一样导入MongooseModule,但改用forFeature()函数,该函数定义将为当前作用域注册数据库模型。

实现GraphQL CRUD

让我们继续使用Mongoose数据库和GraphQL端点实现CRUD功能。

服务

首先,让我们在服务内部创建数据库CRUD功能。

import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { ItemType } from './dto/create-item.dto';
import { Item } from './interfaces/item.interface';
import { ItemInput } from './input-items.input';

@Injectable()
export class ItemsService {
constructor(@InjectModel('Item') private itemModel: Model<Item>) {}

async create(createItemDto: ItemInput): Promise<Item> {
const createdItem = new this.itemModel(createItemDto);
return await createdItem.save();
}

async findAll(): Promise<Item[]> {
return await this.itemModel.find().exec();
}

async findOne(id: string): Promise<Item> {
return await this.itemModel.findOne({ _id: id });
}

async delete(id: string): Promise<Item> {
return await this.itemModel.findByIdAndRemove(id);
}

async update(id: string, item: Item): Promise<Item> {
return await this.itemModel.findByIdAndUpdate(id, item, { new: true });
}
}

在这里,我们首先使用依赖注入将数据库模型导入构造函数中。 之后,我们将继续使用标准MongoDB函数来实现基本的CRUD功能。

解析器

现在,我们已经在服务内部实现了CRUD功能,我们只需要创建GraphQL解析器即可在其中定义GraphQL所需的查询和变化。

src/items/items.resolver.ts

import { Resolver, Query, Mutation, Args } from '@nestjs/graphql';
import { ItemsService } from './items.service';
import { ItemType } from './dto/create-item.dto';
import { ItemInput } from './input-items.input';

@Resolver()
export class ItemsResolver {
constructor(
private readonly itemsService: ItemsService,
) {}

@Query(() => [ItemType])
async items() {
return this.itemsService.findAll();
}

@Mutation(() => ItemType)
async createItem(@Args('input') input: ItemInput) {
return this.itemsService.create(input);
}

@Mutation(() => ItemType)
async updateItem(@Args('id') id: string, @Args('input') input: ItemInput) {
return this.itemsService.update(id, input);
}

@Mutation(() => ItemType)
async deleteItem(@Args('id') id: string) {
return this.itemsService.delete(id);
}

@Query(() => String)
async hello() {
return 'hello';
}
}

如您所见,我们使用不同的方法创建了一个类,这些方法利用了我们先前创建的ItemService。 但是该类还带有一些非常有趣的装饰器,让我们看一下它们:

  • @Resolver() —告诉Nestjs此类知道如何解决我们商品的动作。
  • @Query() — GraphQL中的查询基本上是客户端用来从服务器请求特定字段的构造。 在这种情况下,装饰器只是说我们可以使用函数的名称来查询它,我们将在以后做。
  • @Mutation() — GraphQL中的变化与查询非常相似,但更多的是先变化数据然后查询数据。
  • @Args() —是用于声明输入参数的辅助装饰器

端到端测试应用

现在,我们已经完成了CRUD功能,下面我们来看看如何使用Jest测试库测试应用程序。

如果您以前从未使用过Jest,我建议您先学习基础知识,然后再继续本节。

首先,我们需要使用以下命令创建一个新的测试文件。

touch test/items.e2e-spect.ts 

之后,我们可以继续为模块创建基本测试设置。

import { Test, TestingModule } from '@nestjs/testing';
import * as request from 'supertest';
import { ItemsModule } from '../src/items/items.module';
import { MongooseModule } from '@nestjs/mongoose';
import { GraphQLModule } from '@nestjs/graphql';
import { Item } from '../src/items/interfaces/item.interface';

describe('ItemsController (e2e)', () => {
let app;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [
ItemsModule,
MongooseModule.forRoot('mongodb://localhost/nestgraphqltesting'),
GraphQLModule.forRoot({
autoSchemaFile: 'schema.gql',
}),
],
}).compile();

app = moduleFixture.createNestApplication();
await app.init();
});

afterAll(async () => {
await app.close();
});
}

在此代码块中,我们使用测试AppController所需的三个模块创建一个Nestjs实例。 我们还定义了所有测试完成后将关闭实例。

接下来,我们创建两个商品对象,这些对象将在我们的HTTP请求中使用。

const item: Item = {
title: '一个好东西',
price: 10,
description: '商品描述',
};

let id: string = '';

const updatedItem: Item = {
title: '一个更新后的好东西',
price: 20,
description: '更新后的商品描述',
};

之后,我们可以创建一个GraphQL查询来测试项目创建功能,并在我们的HTTP请求中使用它。

const createitemObject = JSON.stringify(item).replace(
/\"([^(\")"]+)\":/g,
'$1:',
);

const createItemQuery = `
mutation {
createItem(input: ${createitemObject}) {
title
price
description
id
}
}`;

it('createItem', () => {
return request(app.getHttpServer())
.post('/graphql')
.send({
operationName: null,
query: createItemQuery,
})
.expect(({ body }) => {
const data = body.data.createItem;
id = data.id;
expect(data.title).toBe(item.title);
expect(data.description).toBe(item.description);
expect(data.price).toBe(item.price);
})
.expect(200);
});

在这里,我们创建一个查询,并使用request函数将其发送到我们的端点,该函数使我们可以模拟对服务器的HTTP请求。

然后,我们可以使用Expect函数来验证来自请求的响应。

可以对所有端点重复此过程,并为我们提供以下结果。

import { Test, TestingModule } from '@nestjs/testing';
import * as request from 'supertest';
import { ItemsModule } from '../src/items/items.module';
import { MongooseModule } from '@nestjs/mongoose';
import { GraphQLModule } from '@nestjs/graphql';
import { Item } from '../src/items/interfaces/item.interface';

describe('ItemsController (e2e)', () => {
let app;

beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [
ItemsModule,
MongooseModule.forRoot('mongodb://localhost/nestgraphqltesting'),
GraphQLModule.forRoot({
autoSchemaFile: 'schema.gql',
}),
],
}).compile();

app = moduleFixture.createNestApplication();
await app.init();
});

afterAll(async () => {
await app.close();
});

const item: Item = {
title: 'Great item',
price: 10,
description: 'Description of this great item',
};

let id: string = '';

const updatedItem: Item = {
title: 'Great updated item',
price: 20,
description: 'Updated description of this great item',
};

const createitemObject = JSON.stringify(item).replace(
/\"([^(\")"]+)\":/g,
'$1:',
);

const createItemQuery = `
mutation {
createItem(input: ${createitemObject}) {
title
price
description
id
}
}`;

it('createItem', () => {
return request(app.getHttpServer())
.post('/graphql')
.send({
operationName: null,
query: createItemQuery,
})
.expect(({ body }) => {
const data = body.data.createItem;
id = data.id;
expect(data.title).toBe(item.title);
expect(data.description).toBe(item.description);
expect(data.price).toBe(item.price);
})
.expect(200);
});

it('getItems', () => {
return request(app.getHttpServer())
.post('/graphql')
.send({
operationName: null,
query: '{items{title, price, description, id}}',
})
.expect(({ body }) => {
const data = body.data.items;
const itemResult = data[0];
expect(data.length).toBeGreaterThan(0);
expect(itemResult.title).toBe(item.title);
expect(itemResult.description).toBe(item.description);
expect(itemResult.price).toBe(item.price);
})
.expect(200);
});

const updateItemObject = JSON.stringify(updatedItem).replace(
/\"([^(\")"]+)\":/g,
'$1:',
);

it('updateItem', () => {
const updateItemQuery = `
mutation {
updateItem(id: "${id}", input: ${updateItemObject}) {
title
price
description
id
}
}`;

return request(app.getHttpServer())
.post('/graphql')
.send({
operationName: null,
query: updateItemQuery,
})
.expect(({ body }) => {
const data = body.data.updateItem;
expect(data.title).toBe(updatedItem.title);
expect(data.description).toBe(updatedItem.description);
expect(data.price).toBe(updatedItem.price);
})
.expect(200);
});

it('deleteItem', () => {
const deleteItemQuery = `
mutation {
deleteItem(id: "${id}") {
title
price
description
id
}
}`;

return request(app.getHttpServer())
.post('/graphql')
.send({
operationName: null,
query: deleteItemQuery,
})
.expect(({ body }) => {
const data = body.data.deleteItem;
expect(data.title).toBe(updatedItem.title);
expect(data.description).toBe(updatedItem.description);
expect(data.price).toBe(updatedItem.price);
})
.expect(200);
});
});

一切ok,我们只需要使用以下命令运行测试。

npm run test:e2e

测试应用程序

既然我们已经完成了简单的CRUD应用程序的构建,那么就可以使用GraphQL操作面板对其进行测试。 为此,让我们启动服务器,然后导航到我们的操作面板:

npm run start

启动服务器后,您应该可以在http://localhost:3000/graphql上看到GraphQL操作面板。

接下来,让我们继续编写一个用于创建商品的变化操作。

xxx

之后,让我们使用以下简单查询来测试获取功能:

xxx

现在仅保留更新和删除功能。 为此,我们将需要我们先前创建的商品的ID,您可以通过使用我们的商品查询来获取。

xxx

现在,在接下来的两个操作中使用此ID。

更新商品:

xxx

删除商品:

xxx

最后

我希望本文能帮助您了解GraphQL的基础知识以及如何在NestJS中使用它。