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 通关秘籍

相关推荐
跑跑快跑1 分钟前
React vite + less
前端·react.js·less
web1368856587111 分钟前
ctfshow_web入门_命令执行_web29-web39
前端
GISer_Jing18 分钟前
前端面试题合集(一)——HTML/CSS/Javascript/ES6
前端·javascript·html
清岚_lxn19 分钟前
es6 字符串每隔几个中间插入一个逗号
前端·javascript·算法
胡西风_foxww23 分钟前
【ES6复习笔记】Map(14)
前端·笔记·es6·map
星就前端叭23 分钟前
【开源】一款基于SpringBoot的智慧小区物业管理系统
java·前端·spring boot·后端·开源
缘友一世25 分钟前
将现有Web 网页封装为macOS应用
前端·macos·策略模式
刺客-Andy41 分钟前
React 第十九节 useLayoutEffect 用途使用技巧注意事项详解
前端·javascript·react.js·typescript·前端框架
谢道韫6661 小时前
今日总结 2024-12-27
开发语言·前端·javascript
嘤嘤怪呆呆狗1 小时前
【插件】vscode Todo Tree 简介和使用方法
前端·ide·vue.js·vscode·编辑器