Nest 实现 GraphQL 版 TodoList
我们经常用 restful 的接口来开发业务。
比如 GET 请求 /students 查询所有学生,/students/1 查询 id 为 1 的学生
发送 POST、PUT、DETETE 请求分别代表增删改。
其实也可以用 GraphQL 的方式来写接口:
查询:
新增:
增删改查都在一个接口里搞定,并且想要什么数据由前端自己取。
今天我们就用 Nest + GrahQL 做一个 TodoList 的增删改查。
数据存在 mysql 里,用 Prisma 作为 ORM 框架。
npm install -g @nestjs/cli
nest new graphql-todolist
创建个项目,然后我们首先来实现 restful 接口的增删改查。
用 docker 把 mysql 跑起来:
从 docker 官网下载 docker desktop,这个是 docker 的桌面端:
跑起来后,搜索 mysql 镜像(这步需要科学上网),点击 run:
输入容器名、端口映射、以及挂载的数据卷,还要指定一个环境变量:
端口映射就是把宿主机的 3306 端口映射到容器里的 3306 端口,这样就可以在宿主机访问了。
数据卷挂载就是把宿主机的某个目录映射到容器里的 /var/lib/mysql 目录,这样数据是保存在本地的,不会丢失。
而 MYSQL_ROOT_PASSWORD 的密码则是 mysql 连接时候的密码。
跑起来后,我们用 GUI 客户端连上,这里我们用的是 mysql workbench,这是 mysql 官方提供的免费客户端:
连接上之后,点击创建 database:
指定名字、字符集为 utf8mb4,然后点击右下角的 apply。
创建成功之后在左侧就可以看到这个 database 了:
现在还没有表。
我们在 Nest 里用 Prisma 连接 mysql。
进入项目,安装 prisma
npm install prisma --save-dev
执行 prisma init 创建 schema 文件:
npx prisma init
生成了 schema 文件(用来定义 model 的),和 .env 文件:
改下 .env 的配置:
DATABASE_URL="mysql://root:你的密码@localhost:3306/todolist"
并且修改下 schema 里的 datasource 部分:
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
然后创建 model:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
model TodoItem {
id Int @id @default(autoincrement())
content String @db.VarChar(50)
createTime DateTime @default(now())
updateTime DateTime @updatedAt
}
id 自增,content 是长度为 50 的字符串,还有创建时间 createTime、更新时间 updateTime。
执行 prisma migrate dev,它会根据定义的 model 去创建表:
npx prisma migrate dev --name init
它会生成 sql 文件,里面是这次执行的 sql。
然后还会生成 client 代码,用来连接数据库操作这个表。
可以看到,这次执行的 sql 就是 create table 建表语句:
这时候数据库就就有这个表了:
接下来我们就可以在代码里做 CRUD 了。
生成一个 service:
nest g service prisma --flat --no-spec
改下生成的 PrismaService,继承 PrismaClient,这样它就有 crud 的 api 了:
import { Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
constructor() {
super({
log: [
{
emit: 'stdout',
level: 'query'
}
]
})
}
async onModuleInit() {
await this.$connect();
}
}
在 constructor 里设置 PrismaClient 的 log 参数,也就是打印 sql 到控制台。
在 onModuleInit 的生命周期方法里调用 $connect 来连接数据库。
然后在 AppService 里注入 PrismaService,实现 CRUD:
import { Inject, Injectable } from '@nestjs/common';
import { PrismaService } from './prisma.service';
import { CreateTodoList } from './todolist-create.dto';
import { UpdateTodoList } from './todolist-update.dto';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
@Inject(PrismaService)
private prismaService: PrismaService;
async query() {
return this.prismaService.todoItem.findMany({
select: {
id: true,
content: true,
createTime: true
}
});
}
async create(todoItem: CreateTodoList) {
return this.prismaService.todoItem.create({
data: todoItem,
select: {
id: true,
content: true,
createTime: true
}
});
}
async update(todoItem: UpdateTodoList) {
return this.prismaService.todoItem.update({
where: {
id: todoItem.id
},
data: todoItem,
select: {
id: true,
content: true,
createTime: true
}
});
}
async remove(id: number) {
return this.prismaService.todoItem.delete({
where: {
id
}
})
}
}
@Inject 注入 PrismaService,用它来做 CRUD,where 是条件、data 是数据,select 是回显的字段:
然后创建用到的两个 dto 的 class
todolist-create.dto.ts
export class CreateTodoList {
content: string;
}
todolist-update.dto.ts
export class UpdateTodoList {
id: number;
content: string;
}
在 AppController 里引入下,添加几个路由:
import { Body, Controller, Delete, Get, Post, Query } from '@nestjs/common';
import { AppService } from './app.service';
import { CreateTodoList } from './todolist-create.dto';
import { UpdateTodoList } from './todolist-update.dto';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
@Post('create')
async create(@Body() todoItem: CreateTodoList) {
return this.appService.create(todoItem);
}
@Post('update')
async update(@Body() todoItem: UpdateTodoList) {
return this.appService.update(todoItem);
}
@Get('delete')
async delete(@Query('id') id: number) {
return this.appService.remove(+id);
}
@Get('list')
async list() {
return this.appService.query();
}
}
添加增删改查 4 个路由,post 请求用 @Body() 注入请求体,@Query 拿路径中的参数:
把服务跑起来试一下:
npm run start:dev
首先是 list,现在没有数据:
然后添加一个:
服务端打印了 insert into 的 sql:
数据库也有了这条记录:
再加一个:
然后查一下:
接下来试下修改、删除:
再查一下:
没啥问题。
这样,todolist 的 restful 版接口就完成了。
接下来实现 graphql 版本:
安装用到的包:
npm i @nestjs/graphql @nestjs/apollo @apollo/server graphql
然后在 AppModule 里引入下:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PrismaService } from './prisma.service';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver } from '@nestjs/apollo';
@Module({
imports: [
GraphQLModule.forRoot({
driver: ApolloDriver,
typePaths: ['./**/*.graphql'],
})
],
controllers: [AppController],
providers: [AppService, PrismaService],
})
export class AppModule {}
typePaths 就是 schema 文件的路径:
添加一个 todolist.graphql
type TodoItem {
id: Int
content: String
}
input CreateTodoItemInput {
content: String
}
input UpdateTodoItemInput {
id: Int!
content: String
}
type Query {
todolist: [TodoItem]!
queryById(id: Int!): TodoItem
}
type Mutation {
createTodoItem(todoItem: CreateTodoItemInput!): TodoItem!
updateTodoItem(todoItem: UpdateTodoItemInput!): TodoItem!
removeTodoItem(id: Int!): Int
}
语法比较容易看懂,就是定义数据的结构。
在 Query 下定义查询的接口,在 Mutation 下定义增删改的接口。
然后实现 resolver,也就是这些接口的实现:
nest g resolver todolist --no-spec --flat
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { PrismaService } from './prisma.service';
import { Inject } from '@nestjs/common';
import { CreateTodoList } from './todolist-create.dto';
import { UpdateTodoList } from './todolist-update.dto';
@Resolver()
export class TodolistResolver {
@Inject(PrismaService)
private prismaService: PrismaService;
@Query("todolist")
async todolist() {
return this.prismaService.todoItem.findMany();
}
@Query("queryById")
async queryById(@Args('id') id) {
return this.prismaService.todoItem.findUnique({
where: {
id
}
})
}
@Mutation("createTodoItem")
async createTodoItem(@Args("todoItem") todoItem: CreateTodoList) {
return this.prismaService.todoItem.create({
data: todoItem,
select: {
id: true,
content: true,
createTime: true
}
});
}
@Mutation("updateTodoItem")
async updateTodoItem(@Args('todoItem') todoItem: UpdateTodoList) {
return this.prismaService.todoItem.update({
where: {
id: todoItem.id
},
data: todoItem,
select: {
id: true,
content: true,
createTime: true
}
});
}
@Mutation("removeTodoItem")
async removeTodoItem(@Args('id') id: number) {
await this.prismaService.todoItem.delete({
where: {
id
}
})
return id;
}
}
用 @Resolver 声明 resolver,用 @Query 声明查询接口,@Mutation 声明增删改接口,@Args 取传入的参数。
具体增删改查的实现和之前一样。
浏览器访问 http://localhost:3000/graphql 就是 playground,可以在这里查询:
左边输入查询语法,右边是执行后返回的结果。
当然,对新手来说这个 playground 不够友好,没有提示。
我们换一个:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PrismaService } from './prisma.service';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver } from '@nestjs/apollo';
import { TodolistResolver } from './todolist.resolver';
import { ApolloServerPluginLandingPageLocalDefault } from '@apollo/server/plugin/landingPage/default';
@Module({
imports: [
GraphQLModule.forRoot({
driver: ApolloDriver,
typePaths: ['./**/*.graphql'],
playground: false,
plugins: [ApolloServerPluginLandingPageLocalDefault()],
})
],
controllers: [AppController],
providers: [AppService, PrismaService, TodolistResolver],
})
export class AppModule {}
试一下新增:
查询:
修改:
单个查询:
删除:
查询:
基于 GraphQL 的增删改查都成功了!
然后在 react 项目里调用下。
npx create-vite
进入项目,安装 @apollo/client
npm install
npm install @apollo/client
改下 main.tsx
import * as ReactDOM from 'react-dom/client';
import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';
import App from './App';
const client = new ApolloClient({
uri: 'http://localhost:3000/graphql',
cache: new InMemoryCache(),
});
const root = ReactDOM.createRoot(document.getElementById('root')!);
root.render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>,
);
创建 ApolloClient 并设置到 ApolloProvider。
然后在 App.tsx 里用 useQuery 发请求:
import { gql, useQuery } from '@apollo/client';
const getTodoList = gql`
query Query {
todolist {
content
id
}
}
`;
type TodoItem = {
id: number;
content: string;
}
type TodoList = {
todolist: Array<TodoItem>;
}
export default function App() {
const { loading, error, data } = useQuery<TodoList>(getTodoList);
if (loading) return 'Loading...';
if (error) return `Error! ${error.message}`;
return (
<ul>
{
data?.todolist?.map(item => {
return <li key={item.id}>{item.content}</li>
})
}
</ul>
);
}
把服务跑起来:
npm run dev
这里涉及到的跨域,现在后端服务里开启下跨域支持:
可以看到,返回了查询结果:
然后加一下新增:
用 useMutation 的 hook,指定 refetchQueries 也就是修改完之后重新获取数据。
调用的时候传入 content 数据。
import { gql, useMutation, useQuery } from '@apollo/client';
const getTodoList = gql`
query Query {
todolist {
content
id
}
}
`;
const createTodoItem = gql`
mutation Mutation($todoItem: CreateTodoItemInput!) {
createTodoItem(todoItem: $todoItem) {
id
content
}
}
`;
type TodoItem = {
id: number;
content: string;
}
type TodoList = {
todolist: Array<TodoItem>;
}
export default function App() {
const { loading, error, data } = useQuery<TodoList>(getTodoList);
const [createTodo] = useMutation(createTodoItem, {
refetchQueries: [getTodoList]
});
async function onClick() {
await createTodo({
variables: {
todoItem: {
content: Math.random().toString().slice(2, 10)
}
}
})
}
if (loading) return 'Loading...';
if (error) return `Error! ${error.message}`;
return (
<div>
<button onClick={onClick}>新增</button>
<ul>
{
data?.todolist?.map(item => {
return <li key={item.id}>{item.content}</li>
})
}
</ul>
</div>
);
}
测试下:
数据库里也可能看到新增的数据:
这样,我们就能在 react 项目里用 graphql 做 CRUD 了。
案例代码上传了 github。
后端代码: https://github.com/QuarkGluonPlasma/nestjs-course-code/tree/main/graphql-todolist
前端代码:https://github.com/QuarkGluonPlasma/nestjs-course-code/tree/main/graphql-todolist-client
总结
我们实现了Restful 和GraphQL 版的 CRUD。
前端用 React + @apollo/client。
后端用 Nest + GraphQL + Prisma + MySQL。
GraphQL 主要是定义 schema 和 resolver 两部分,schema 是 Query、Mutation 的结构,resolver 是它的实现。
可以在 playground 里调用接口,也可以在 react 里用 @appolo/client 调用。
相比 restful 的版本,graphql 只需要一个接口,然后用查询语言来查,需要什么数据取什么数据,更加灵活。
业务开发中,你会选择用 GraphQL 开发接口么?