简介
用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/swagger
的DocumentBuilder
创建
3. 继续开发todo-ui
未完待续...