用Pnpm+Turbo实现一个前后端共享api的Todos项目

简介

用pnpm、turbo搭建monorepo项目,服务端nestjs、客户端vue,通过ts-rest共享前、后端Typescript Api类型。

执行turbo命令,比如turbo run dev,需要workspace的包内都包含该命令,比如:nestjs的开发命令为start:dev,vite项目为dev,那么就会只运行vite项目内的dev

1. 新建项目

bash 复制代码
# 新建文件夹
mkdir todos-monorepo

# 进入目录
cd todos-monorepo

# 初始化package.json
pnpm init 

# 新建pnpm-workspace.yaml
touch pnpm-workspace.yaml

# 新建turbo.json
touch turbo.json

# 创建apps和packages文件夹,存放app和共享包
mkdir apps
mkdir packages
  • package.json配置
json 复制代码
// todos-monorepo/package.json
{
  "name": "todos-monorepo",
  "version": "1.0.0",
  "description": "",
  "scripts": {
    "dev": "turbo run dev",
    "build": "turbo run build",
    "build:release": "turbo run build --no-cache"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "turbo": "^1.13.2"
  }
}
  • pnpm-workspace.yaml配置
yaml 复制代码
# todos-monorepo/pnpm-workspace.yaml
packages:
  - 'apps/**'
  - 'packages/*'
  • turbo.json配置
json 复制代码
// todos-monorepo/turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

2. 依赖安装和初始化项目

2.1 安装依赖

bash 复制代码
# todos-monorepo根目录安装
pnpm i turbo -D

2.2 初始化服务端nestjs项目和客户端ui项目

bash 复制代码
# 进入apps目录
cd apps

# 用nest-cli初始化api项目
nest new todo-api

# 用pnpm初始化基于vite的vue+ts项目
pnpm create vite
  • 此时项目结构如下图

2.3 建立packages内shared-api包并配置

bash 复制代码
# 切换到packages目录
cd packages

# 创建共享api包
mkdir shared-api

# 进入shared-api文件夹
cd shared-api

# 创建package.json文件,配置在下面
pnpm init 

# 创建tsconfig.json,配置在下面
touch tsconfig.json

# 创建src内的index.ts
mkdir src && touch src/index.ts文件

# 安装shared-api的依赖
pnpm add @ts-rest/core typescript zod
  • package.json配置
json 复制代码
// todos-monorepo/packages/shared-api/package.json
{
  "name": "shared-api",
  "version": "1.0.0",
  "description": "",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "scripts": {
    "dev": "tsc --watch",
    "build": "tsc"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@ts-rest/core": "^3.44.0",
    "typescript": "^5.4.5",
    "zod": "^3.22.4"
  }
}
  • tsconfig.json配置
json 复制代码
// todos-monorepo/packages/shared-api/tsconfig.json
{
  "compilerOptions": {
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "isolatedModules": true,
    "module": "CommonJS",
    "preserveWatchOutput": true,
    "skipLibCheck": true,
    "strict": true,
    "rootDir": "src",
    "outDir": "dist",
    "declaration": true
  },
  "exclude": ["node_modules", "./dist/**/*"]
}
typescript 复制代码
// todos-monorepo/packages/shared-api/src/index.ts
import { initContract } from "@ts-rest/core";
import { z } from "zod";

const c = initContract();

export const TodoSchema = z.object({
  id: z.number(),
  title: z.string(),
  description: z.string(),
  completed: z.boolean(),
});

export type Todo = z.infer<typeof TodoSchema>;

export const contract = c.router(
  {
    todos: {
      create: {
        method: "POST",
        path: "/todos",
        body: TodoSchema.omit({ id: true }),
        responses: {
          201: TodoSchema,
        },
      },
      getAll: {
        method: "GET",
        path: "/todos",
        query: z.object({
          title: z.string().optional(),
        }),
        responses: {
          200: TodoSchema.array(),
        },
      },
      getOne: {
        method: "GET",
        path: "/todos/:id",
        pathParams: z.object({
          id: z.coerce.number(),
        }),
        responses: {
          200: TodoSchema,
          404: z.object({
            message: z.string(),
          }),
        },
      },
      update: {
        method: "PATCH",
        path: "/todos/:id",
        pathParams: z.object({
          id: z.coerce.number(),
        }),
        body: TodoSchema.omit({ id: true }).partial(),
        responses: {
          200: TodoSchema,
          404: z.object({
            message: z.string(),
          }),
        },
      },
      remove: {
        method: "DELETE",
        path: "/todos/:id",
        pathParams: z.object({
          id: z.coerce.number(),
        }),
        body: z.any(),
        responses: {
          204: z.object({}),
          404: z.object({
            message: z.string(),
          }),
        },
      },
    },
  },
  {
    pathPrefix: "/api",
    strictStatusCodes: true,
  }
);

export type Contract = typeof contract;

export type UpdateTodoDto = z.infer<Contract["todos"]["update"]["body"]>;

2.4 配置todo-api

进入todos-monorepo/apps/todo-api目录,修改start:dev命令为dev,和根目录package.json内配置的turbo run dev命令保持一致。

json 复制代码
// todos-monorepo/apps/todo-api/package.json
"scripts": {
     // 主要修改地方
    "dev": "nest start --watch"
     // 其它部分
    "build": "nest build",
    "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
    "start": "nest start",
    "start:debug": "nest start --debug --watch",
    "start:prod": "node dist/main",
    "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:cov": "jest --coverage",
    "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
    "test:e2e": "jest --config ./test/jest-e2e.json"
 },
  • 此时在终端,切换到根目录执行pnpm run dev会看到下图所示,两个项目都一起运行起来了:

2.5 给todo-api安装shared-api包

bash 复制代码
# todos-monorepo目录执行
pnpm add shared-api --filter todo-api

安装正常,会看到package.json内安装的shared-api依赖

2.6 用nest-cli生成todos模块

bash 复制代码
# todos-monorepo/apps/todo-api目录,生成todos模块目录
nest g res todos

# todos-monorepo/apps/todo-api目录,安装依赖包
pnpm add @ts-rest/nest @ts-rest/core @ts-rest/open-api @nestjs/swagger
  • todos.controller.ts代码
typescript 复制代码
// todos-monorepo/apps/todo-api/todos/todos.controller.ts
import { Controller } from '@nestjs/common';
import { TodosService } from './todos.service';
import { TsRestHandler, tsRestHandler } from '@ts-rest/nest';
import '@ts-rest/core';
import { contract } from 'shared-api';

@Controller()
export class TodosController {
  constructor(private readonly todosService: TodosService) {}

  @TsRestHandler(contract.todos)
  async handler() {
    return tsRestHandler(contract.todos, {
      create: async ({ body }) => {
        return {
          status: 201,
          body: this.todosService.create(body),
        };
      },
      getAll: async ({ query: { title } }) => {
        return {
          status: 200,
          body: this.todosService.getAll(title),
        };
      },
      getOne: async ({ params: { id } }) => {
        const item = await this.todosService.getOne(id);
        if (!item) {
          return {
            status: 404,
            body: {
              message: '没有item',
            },
          };
        }
        return {
          status: 200,
          body: item,
        };
      },
      update: async ({ params: { id }, body }) => {
        const item = await this.todosService.update(id, body);
        if (!item) {
          return {
            status: 404,
            body: {
              message: '没有item',
            },
          };
        }
        return {
          status: 200,
          body: item,
        };
      },
      remove: async ({ params: { id } }) => {
        const item = await this.todosService.remove(id);
        if (!item) {
          return {
            status: 404,
            body: {
              message: '没有item',
            },
          };
        }
        return {
          status: 204,
          body: item,
        };
      },
    });
  }
}
  • todos.service.ts代码
typescript 复制代码
// todos-monorepo/apps/todo-api/todos/todos.service.ts
import { Injectable } from '@nestjs/common';
import { Todo, UpdateTodoDto } from 'shared-api';

@Injectable()
export class TodosService {
  todos: Todo[] = [
    {
      id: 1,
      title: 'Todo 1',
      description: 'Todo 1 description',
      completed: false,
    },
    {
      id: 2,
      title: 'Todo 2',
      description: 'Todo 2 description',
      completed: false,
    },
    {
      id: 3,
      title: 'Todo 3',
      description: 'Todo 3 description',
      completed: false,
    },
  ];

  create(todo: Omit<Todo, 'id'>) {
    const newTodo = {
      ...todo,
      id: Date.now(),
    };
    this.todos.push(newTodo);
    return newTodo;
  }

  getAll(title?: string) {
    if (title) {
      return this.todos.filter((todo) =>
        todo.title.toLowerCase().includes(title.toLowerCase()),
      );
    }
    return this.todos;
  }

  getOne(id: number) {
    return this.todos.find((todo) => todo.id === id);
  }

  // update(id: number, dto: Partial<Todo>) {
  update(id: number, dto: UpdateTodoDto) {
    const index = this.todos.findIndex((todo) => todo.id === id);
    if (index === -1) {
      return null;
    }
    this.todos[index] = { ...this.todos[index], ...dto };
    return this.todos[index];
  }

  remove(id: number) {
    const index = this.todos.findIndex((todo) => todo.id === id);
    if (index === -1) {
      return null;
    }
    this.todos = this.todos.filter((todo) => todo.id !== id);
    return {};
  }
}
  • main.ts代码
typescript 复制代码
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

import { SwaggerModule } from '@nestjs/swagger';
import { generateOpenApi } from '@ts-rest/open-api';
import { contract } from 'shared-api';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const openApiDocument = generateOpenApi(contract, {
    info: {
      title: 'Todos API',
      version: '1.0.0',
    },
  });
  SwaggerModule.setup('openapi', app, openApiDocument);

  await app.listen(3000);
  
  console.log(`服务端运行在 http://localhost:3000`);
}
bootstrap();
  • 此时打开http://localhost:3000/openapi地址会看到自动配置好的swagger文档

提示:这里没有用@nestjs/swaggerDocumentBuilder创建

3. 继续开发todo-ui

未完待续...

相关链接

turbo.build/repo/docs

pnpm.io/zh/

www.youtube.com/watch?v=aEe...

相关推荐
_.Switch4 分钟前
Python Web 与物联网(IoT)集成与实时数据处理
开发语言·前端·python·物联网·架构·log4j
霍金的微笑36 分钟前
MYSQL(学习笔记)
java·前端·数据库
软糖工程0011 小时前
XML简介
xml·前端·学习
Clarca1 小时前
Vue源码探究虚拟DOM
前端·javascript·vue.js·ecmascript
小小李程序员2 小时前
CSS实现磨砂玻璃效果
前端·css
java—大象2 小时前
基于JavaWeb开发的Java+SpringMvc+vue+element实现驾校管理系统详细设计
java·前端·vue.js·spring boot·mybatis·课程设计
fishmemory7sec2 小时前
electron介绍
前端·javascript·electron
Passion不晚2 小时前
Java 全栈开发中的最佳实践:从前端到后端的完整流程
java·前端·全栈
Moon里4 小时前
【CSS】字体文本
前端·css
Jonathan Star8 小时前
两个圆形 一个z里面一个z外面,z里面的大,颜色不同 html
前端·html