用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...

相关推荐
wordbaby2 分钟前
搞不懂 px、dpi 和 dp?看这一篇就够了:图解 RN 屏幕适配逻辑
前端
程序员爱钓鱼4 分钟前
使用 Node.js 批量导入多语言标签到 Strapi
前端·node.js·trae
鱼樱前端5 分钟前
uni-app开发app之前提须知(IOS/安卓)
前端·uni-app
V***u4535 分钟前
【学术会议论文投稿】Spring Boot实战:零基础打造你的Web应用新纪元
前端·spring boot·后端
i听风逝夜44 分钟前
Web 3D地球实时统计访问来源
前端·后端
iMonster1 小时前
React 组件的组合模式之道 (Composition Pattern)
前端
呐呐呐呐呢1 小时前
antd渐变色边框按钮
前端
元直数字电路验证1 小时前
Jakarta EE Web 聊天室技术梳理
前端
wadesir1 小时前
Nginx配置文件CPU优化(从零开始提升Web服务器性能)
服务器·前端·nginx
牧码岛1 小时前
Web前端之canvas实现图片融合与清晰度介绍、合并
前端·javascript·css·html·web·canvas·web前端