Nest 实现 GraphQL 版 TodoList

我们经常用 restful 的接口来开发业务。

比如 GET 请求 /students 查询所有学生,/students/1 查询 id 为 1 的学生

发送 POST、PUT、DETETE 请求分别代表增删改。

其实也可以用 GraphQL 的方式来写接口:

查询:

新增:

增删改查都在一个接口里搞定,并且想要什么数据由前端自己取。

今天我们就用 Nest + GrahQL 做一个 TodoList 的增删改查。

数据存在 mysql 里,用 Prisma 作为 ORM 框架。

bash 复制代码
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

bash 复制代码
npm install prisma --save-dev

执行 prisma init 创建 schema 文件:

bash 复制代码
npx prisma init

生成了 schema 文件(用来定义 model 的),和 .env 文件:

改下 .env 的配置:

env 复制代码
DATABASE_URL="mysql://root:你的密码@localhost:3306/todolist"

并且修改下 schema 里的 datasource 部分:

javascript 复制代码
datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

然后创建 model:

javascript 复制代码
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 取创建表:

csharp 复制代码
npx prisma migrate dev --name init

它会生成 sql 文件,里面是这次执行的 sql。

然后还会生成 client 代码,用来连接数据库操作这个表。

可以看到,这次执行的 sql 就是 create table 建表语句:

这时候数据库就就有这个表了:

接下来我们就可以在代码里做 CRUD 了。

生成一个 service:

css 复制代码
nest g service prisma --flat --no-spec

改下生成的 PrismaService,继承 PrismaClient,这样它就有 crud 的 api 了:

javascript 复制代码
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:

javascript 复制代码
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

javascript 复制代码
export class CreateTodoList {
    content: string;
}

todolist-update.dto.ts

typescript 复制代码
export class UpdateTodoList {
    id: number;
    content: string;
}

在 AppController 里引入下,添加几个路由:

javascript 复制代码
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 拿路径中的参数:

把服务跑起来试一下:

arduino 复制代码
npm run start:dev

首先是 list,现在没有数据:

然后添加一个:

服务端打印了 insert into 的 sql:

数据库也有了这条记录:

再加一个:

然后查一下:

接下来试下修改、删除:

再查一下:

没啥问题。

这样,todolist 的 restful 版接口就完成了。

接下来实现 graphql 版本:

安装用到的包:

bash 复制代码
npm i @nestjs/graphql @nestjs/apollo @apollo/server graphql

然后在 AppModule 里引入下:

javascript 复制代码
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

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,也就是这些接口的实现:

bash 复制代码
nest g resolver todolist --no-spec --flat
javascript 复制代码
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 不够友好,没有提示。

我们换一个:

javascript 复制代码
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 项目里调用下。

bash 复制代码
npx create-vite

进入项目,安装 @apollo/client

bash 复制代码
npm install

npm install @apollo/client

改下 main.tsx

javascript 复制代码
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 发请求:

javascript 复制代码
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>
  );
}

把服务跑起来:

arduino 复制代码
npm run dev

这里涉及到的跨域,现在后端服务里开启下跨域支持:

可以看到,返回了查询结果:

然后加一下新增:

用 useMutation 的 hook,指定 refetchQueries 也就是修改完之后重新获取数据。

调用的时候传入 content 数据。

javascript 复制代码
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。

后端代码: github.com/QuarkGluonP...

前端代码:github.com/QuarkGluonP...

总结

我们实现了一个 GraphQL 版的 CRUD。

前端用 React + @apollo/client。

后端用 Nest + GraphQL + Prisma + MySQL。

GraphQL 主要是定义 schema 和 resolver 两部分,schema 是 Query、Mutation 的结构,resolver 是它的实现。

可以在 playground 里调用接口,也可以在 react 里用 @appolo/client 调用。

相比 restful 的版本,graphql 只需要一个接口,然后用查询语言来查,需要什么数据取什么数据,更加灵活。

业务开发中,你会选择用 GraphQL 开发接口么?

更多内容可以看我的小册 Nest 通关秘籍

相关推荐
掘金一周3 分钟前
金石焕新程 >> 瓜分万元现金大奖征文活动即将回归 | 掘金一周 4.3
前端·人工智能·后端
三翼鸟数字化技术团队21 分钟前
Vue自定义指令最佳实践教程
前端·vue.js
Jasmin Tin Wei1 小时前
蓝桥杯 web 学海无涯(axios、ecahrts)版本二
前端·蓝桥杯
Misnearch1 小时前
node.js版本管理
node.js
圈圈编码1 小时前
Spring Task 定时任务
java·前端·spring
转转技术团队1 小时前
代码变更暗藏危机?代码影响范围分析为你保驾护航
前端·javascript·node.js
Mintopia1 小时前
Node.js高级实战:自定义流与Pipeline的高效数据处理 ——从字母生成器到文件管道的深度解析
前端·javascript·node.js
Mintopia1 小时前
Three.js深度解析:InstancedBufferGeometry实现动态星空特效 ——高效渲染十万粒子的底层奥秘
前端·javascript·three.js
北凉温华1 小时前
强大的 Vue 标签输入组件:基于 Element Plus 的 ElTagInput 详解
前端
原生高钙1 小时前
LLM大模型对话框实践:大文件的分片上传
前端