tsoa基础使用
项目结构
目录结构大致如下
src/
├─ controllers/
│ └─ user.controller.ts
├─ routes/
│ └─ routes.ts ← tsoa 生成
├─ services/
│ └─ user.service.ts
├─ app.ts
ORM框架生成基本信息
使用prisma
依据数据库表结构,生成基本的实体(vo层)和操作数据库的操作(dao层)代码,这里不再多做结介绍!
安装和配置tsoa
安装tsoa
pnpm add tsoa @tsoa/runtime
根目录创建配置文件 tsoa.json
{
"entryFile": "src/app.ts", // 程序入口
"noImplicitAdditionalProperties": "throw-on-extras",
"controllerPathGlobs": ["src/controllers/*"], // 扫描controllers生成router
"spec": {
"outputDirectory": ".", // openApi文档生成路径
"specVersion": 3
},
"routes": {
"routesDir": "src/routers", // router生成路径
"middleware": "koa" // 使用框架
}
}
编写一个控制器
controller/user.controller.ts
import { queryOne } from "@/service/user";
import { user as User } from '@prisma/client';
import { Controller, Get, Path, Route, } from "tsoa";
@Route("user")
export class UserController extends Controller {
@Get("{userId}")
public async getUser(@Path() userId: number): Promise<User> {
return queryOne({ id: userId });
}
}
路由生成与使用
运行 npx tsoa routes
,会根据你的配置路径生成 路由文件,比如我的在
src
|--router
|--routes.ts
在入口程序中使用它 app.ts
...
import Router from "@koa/router";
import { RegisterRoutes } from "@/router/routes";
const app = new Koa();
...
const router = new Router();
RegisterRoutes(router);
app.use(router.routes());
app.listen(3003, '0.0.0.0', () => {
const { green, blue, bold } = chalk;
const str = `${green('➜')} ${bold('后端服务已启动:')} ${blue('http://localhost:3003')}`;
console.log(str);
});
openApi与swagger
运行 npx tsoa spec
,会根据你的配置路径生成 openApi文档文件,比如我的在根目录下 swagger.json
如果你想在运行时获取,可以使用 generateXX
import {generateRoutes,generateSpec,ExtendedRoutesConfig,ExtendedSpecConfig} from "tsoa";
(async () => {
const specOptions: ExtendedSpecConfig = {
basePath: "/api",
entryFile: "./app.ts",
specVersion: 3,
outputDirectory: ".",
controllerPathGlobs: ["src/controllers/*"],
};
const routeOptions: ExtendedRoutesConfig = {
basePath: "src/routers",
entryFile: "./app.ts",
routesDir: "./api",
};
await generateSpec(specOptions);
await generateRoutes(routeOptions);
})();
其它
service
如上操作之后,你的router已经按照class的写法了,所以service层最好也统一一下吧,你也不想打破这份协调,对吧?!
services/user.service.ts
import { user as User, PrismaClient } from '@prisma/client';
// 给post请求(创建用户)用的类型定义,不应该包含 id.
export type UserCreationParams = Pick<User, "email" | "name" | "phoneNumbers">;
const prisma = new PrismaClient();
export class UsersService {
public get(id: number): User {
const result = await prisma.user.findFirst({where: { id }});
return result;
}
public create(userCreationParams: UserCreationParams): User {
return {
id: Math.floor(Math.random() * 10000), // Random
status: "Happy",
...userCreationParams,
};
}
}
同时生成
如果你既想生成路由,也想生成 openApi文档,可以
npx tsoa spec-and-routes
实时生成
tsx watch 只能「重启 Node 进程」,不会帮你重新执行 tsoa spec-and-routes
所以单纯 tsx watch src/index.ts 不会让路由 / OpenAPI 更新。
如果你需要监听controller里的文件变化,实时更新路由和openApi文档,可以使用 chokidar cli插件实时监听文件变化,安装之后可以这么用
"tsoa:gen": "tsoa spec-and-routes",
"tsoa:watch": "npx chokidar 'src/controllers/**' -c 'npm run tsoa:gen'" // mac下
注意,在win下,单引号需要替换为双引号(建议这个,mac亦可用)
"tsoa:watch": "npx chokidar \"src/controllers/**\" -c \"npm run tsoa:gen\""
并行运行
我想程序启动的时候,先执行一遍 tsoa 生成,然后再监听改变持续生成,最后再启动项目。
因为涉及到多个同时执行程序,所以 靠&& 是不行的,因为它的作用是串行而非并行,
这个时候就用到了 concurrently,安装后使用
pacakge.json
"scripts": {
"dev_gen": "npm run clean && concurrently \"npm run tsoa:gen\" \"npm run tsoa:watch\" \"npm run dev\"",
"dev": "npx kill-port 3003 || true && tsx watch --clear-screen=false --include ./src/*.ts ./src/app.ts",
"start": "npx kill-port 3003 && tsx ./src/app.ts",
"prisma": "tsx ./bin/prisma.ts",
"tsoa:gen": "tsoa spec-and-routes",
"tsoa:watch": "npx chokidar \"src/controllers/**\" -c \"npm run tsoa:gen\"",
"clean": "pkill -f chokidar || true && pkill -f tsx || true"
},
允许 ts 装饰器
{
"compilerOptions": {
...
/* 1. 允许在类及其成员上使用装饰器语法 */
"experimentalDecorators": true,
/* 2. 为装饰器发射类型元数据(如设计类型、参数类型、返回类型) */
"emitDecoratorMetadata": true,
}
}
Controller undefined
启动的时候报错
@Route("api")
export class RootController extends Controller {}
// 得到一下报错
Class extends value undefined is not a constructor or null
解决办法,在 tsconfig.json 添加如下配置即可
{
"compilerOptions": {
...
"paths": {
...
"tsoa": ["node_modules/tsoa/dist"],
"tsoa/": ["node_modules/tsoa/dist/"]
}
}
}
路径别名问题
tsx早在2024年就已经支持 tsconfig.json 中的路径别名,但是这并不适用用 tsoa
import { Controller, Get, Path, Route } from 'tsoa';
import JsonResult, { JsonResultType } from '@/utils/json-result';
@Route('api')
export class RootController extends Controller {
@Get()
public async hi1(): Promise<JsonResultType<string>> {
return JsonResult.success('hello, this is server');
}
}
如上代码将报错,找不到 @/utils/json-result
enerate routes error.
[0] GenerateMetadataError: No declarations found for referenced type JsonResultType.
解决办法很简单,不用别名即可
koaBody
在koa项目中,当你使用tsoa,并且控制器中使用到了 @Body接受参数的的时候,tsoa要求你必须使用koaBody插件,否则会报错 request body is required
,其实这个早就有人发现了,并质疑作者为何不在官方文档中声明
import koaBody from 'koa-body';
app.use(koaBody());