@ builtvision/nest- sdk - 生成器
生成全类型 RTK 查询 您的 NestJS 控制器中的 API 客户端 .
生成器用 TypeScript 编译器读取您的 API 的源代码: 没有运行的服务器, 没有 OpenAPI 光谱, 没有添加的装饰器 。 每条路由都成为了RTK查询端点,其参数和结果由控制器本身输入,包括返回类型 TypeScript 推断出您的服务.
// API: users.controller.ts
@Controller('users')
export class UsersController {
@Get(':id')
async getUser(@Param('id') id: string) {
return this.usersService.findOne(id) // Promise<User | null>
}
@Patch(':id')
renameUser(@Param('id') id: string, @Body('name') name: string) { ... }
}
// Client: a React component
const { data: user } = API.Users.getUser.useQuery({ id }) // user: User | null | undefined
const [renameUser] = API.Users.renameUser.useMutation()
renameUser({ id, name: 'Jane' }) // PATCH /users/:id with body { name }
renameUser({ id }) // Type error: `name` is missing
目录
- 所需资源
- 开始
- 控制器如何成为终点
- 生成的文件
- 配置
- 请求函数
- 添加缓存( 塔克)
- 命令行
- 分析引擎
- 建议的工作流程
- 限制
- 解决问题
- 普通 SDK 模式 (
--generate(中文(简体) ) - 从 2.x 升级
- 发展
- 信贷和许可证
所需资源
- 用 TypeScript 编写的 NestJS API 带有
tsconfig.json - 能看到 API 源代码的客户端 (通常为单列) : 生成的文件直接从 API 源导入您的 DTO 和实体
import type,因此客户端的 TypeScript 项目必须能够键入这些文件(见 客户端类型脚本设置(中文(简体) ) - Redux 工具包 2.x 反应( A)
@reduxjs/toolkit/query/react在客户端中 - 节点20.19+或22.12+ 运行生成器( 旧版本返回到较慢) 引擎(中文(简体) )
开始
这些例子假设一个单曲与 API 在 api/ 和反应应用程序 web/。 。 。 .
1. 安装
cd web
npm install --save-dev @buildvision/nest-sdk-generator
2. 创建配置文件
web/api-gen.config.json (路径与此文件相对):
{
"apiInputPath": "../api",
"sdkOutputPath": "./src/api/endpoints",
"sdkInterfacePath": "./src/api/BaseRequest.ts",
"generateTimestamps": false,
"controllerOutput": {
"fileName": { "removeSuffix": ".controller", "addSuffix": ".endpoints" },
"exportName": { "removeSuffix": "Controller", "addSuffix": "Endpoints" },
"endpointGroupName": { "removeSuffix": "Controller" }
}
}
所有选项均在 配置。 。 。 .
3. 写入请求功能
每一个端点都会调用您拥有的单个函数, 它执行 HTTP 请求: 这是基础 URL 、 认证和错误处理的活性 。 创建文件 sdkInterfacePath 指(参见 请求函数 一个完整的例子:
// web/src/api/BaseRequest.ts
import type { BaseQueryFn } from '@reduxjs/toolkit/query'
export type BaseRequestArgs = {
body: unknown
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
query: Record<string, any>
route: string
}
export type BaseRequestFn = BaseQueryFn<BaseRequestArgs, unknown, { status: number; message: string }>
export const BaseRequest: BaseRequestFn = async ({ body, method, query, route }) => {
// ...perform the request, return { data } or { error }
}
如果文件不存在, 生成器会为您创建启动器执行( 见 generateDefaultSdkInterface) (中文(简体) ).
4. 生成客户
npx nest-sdk-generator api-gen.config.json --generate-rtk
5. 创建 RTK 查询 API
// web/src/api/index.ts
import { configureStore } from '@reduxjs/toolkit'
import { createApi } from '@reduxjs/toolkit/query/react'
import { BaseRequest } from './BaseRequest'
import endpoints from './endpoints'
export const rtkApi = createApi({
reducerPath: 'api',
baseQuery: BaseRequest,
endpoints: endpoints.buildRTKEndpoints,
})
// Endpoints grouped by controller: API.Users.getUser, API.Comments.addComment...
export const API = endpoints.groupRTKEndpoints(rtkApi)
export const store = configureStore({
reducer: { [rtkApi.reducerPath]: rtkApi.reducer },
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(rtkApi.middleware),
})
将您的应用包入反应- redux 中 <Provider store={store}> 和往常一样.
6. 使用终点
import { API } from './api'
export function Comments({ articleId }: { articleId: string }) {
const { data: comments, isLoading } = API.Comments.listComments.useQuery({ articleId })
const [addComment, { isLoading: isSaving }] = API.Comments.addComment.useMutation()
if (isLoading) return <p>Loading...</p>
return (
<>
<ul>
{comments?.map((comment) => (
<li key={comment.id}>{comment.text}</li>
))}
</ul>
<button disabled={isSaving} onClick={() => addComment({ articleId, text: 'Nice!' })}>
Comment
</button>
</>
)
}
每个被分组的端点都是常规的RTK查询端点,所以所有RTK查询提供的工作: useQuery, (中文(简体) ), useLazyQuery, (中文(简体) ), useMutation, (中文(简体) ), initiate, (中文(简体) ), select, (中文(简体) ), rtkApi.util.
客户端类型脚本设置
生成的文件包含 import type 语句指向您的 API 源文件, 而客户端的编译器会检查这些文件 带有客户端编译器选项。实际上,客户 tsconfig.json 需求:
"experimentalDecorators": true,作为 Nest 的参数装饰器(@Param(), (中文(简体) ),@Body()...)只对遗留的装饰者有效- API 的选项会影响 DTO 和实体的类型检查,最常见的是
"strictPropertyInitialization": false - API的路径别名( :
paths),如果其文件通过化名导入彼此,例如:@api/* - API类型所依赖的软件包的访问(例如.
class-validator, 您的 ORM : 它们从 API 的目录中解析出来, 所以只要 API 的依赖性安装就能找到它们
没有 API 代码会最终在您的客户端捆绑中: 只有类型被导入 .
控制器如何成为终点
摄取者. 每个 *.controller.ts aPI的 TypeScript 项目的文件(按其 tsconfig 定义) 被分析。 头一类文件必须有一个 @Controller() 调用无参数或字符串字元的装饰符( q)@Controller('users'); 其他文件被跳过。 每个控制器属于在最接近时声明的模块 *.module.ts 文件,在控制器的目录或父目录中.
路线. 确切的方法之一 @Get(), (中文(简体) ), @Post(), (中文(简体) ), @Put(), (中文(简体) ), @Patch() 或者说 @Delete(),没有参数或字符串字面的调用,成为终点。 路由是控制器的前缀,然后是方法的路由: @Controller('users') + 键 @Get(':id/avatar') 变成 /users/:id/avatar没有这种装饰的方法被忽略了.
辩相. 端点取一个单一对象,将路由的所有输入合并:
| 控制器参数 | 终点参数 | 发送为 |
|---|---|---|
@Param('id') id: string | { id: string } | 内插到路线 |
@Query('page') page: number | { page: number } | 查询字符串( 只有 GET) |
@Query() query: ListQuery | ListQuery | 查询字符串( 只有 GET) |
@Body('name') name: string | { name: string } | JSON 体 (仅非GET) |
@Body() dto: CreateUserDto | CreateUserDto | JSON 体 (仅非GET) |
@Req(), (中文(简体) ), @Headers()定制的装饰器... | - 说吧 | 已忽略 |
譬如说, update(@Param('id') id: string, @Body() dto: UpdateUserDto) 生成端点 { id: string } & UpdateUserDto。没有输入的路径 void。 。 。 .
结果. 结果类型是方法的返回类型 Promise<> 未被包装,完全如TypeScript所见,无论是声明还是推断. 返回方法 this.usersService.findOne(id) 获得服务返回类型.
RTK腾讯. GET 路径变为查询( e)builder.query),所有其他方法都成为了变异(builder.mutation) (中文(简体) ). 端点的名称是控制器方法的名称,所以 方法名称必须在所有控制器中独有:RTK查询将API的所有端点都保留在一个单名空间.
鉴定. 当一个路由无法可靠地表示时, 生成失败, 有明显的错误, 例如 : GET 路径为 a @Body()一个非..GET 路径为 a @Query(),从 URL 中缺少一个路由参数,一个两次使用的参数,或者一个通配符路由.
生成的文件
由于上面的配置,输出目录包含:
endpoints/
├── index.ts # Combines all controllers
├── baseRequest.ts # Re-exports the types of your request function
├── users.endpoints.ts # One file per controller
└── comments.endpoints.ts
每个控制器文件导出一个类,其分类为:
queries财务报告和审定财务报表mutations: 函数将端点的参数转换为请求函数({ body, method, query, route }(中文(简体) )buildRTKEndpoints(builder): 控制器的 RTK 查询端点定义groupRTKEndpoints(api): 从创建的 API 中选择控制器的端点
index.ts 默认导出每个控制器类加:
buildRTKEndpoints: 所有端点定义,传递给createApi({ endpoints })groupRTKEndpoints(api): 由控制器组合的所创建的 API 端点( )API.Users.getUser(中文(简体) )
也出口 GeneratedAPIType 类型,创建的 API 类型.
每个端点都用其HTTP方法,路由和源控制器方法来记录,因此在编辑器中徘徊一个端点显示其来源:
/**
* GET /articles/:articleId/comments <=> CommentsController.listComments
*
* file:///./../../../api/src/articles/comments/comments.controller.ts (cmd + click)
*/
listComments: builder.query<Comment[], { articleId: string }>({
query: this.queries.listComments,
}),
每次运行时生成的文件都会被覆盖:不要编辑.
配置
配置文件是一个JSON文件. 相对路径由配置文件的目录解决.
路径( 需要)
| 选项 | 说明 |
|---|---|
apiInputPath | 雀巢JS API目录,包含其 tsconfig.json |
sdkOutputPath | 要生成客户端的目录 。 没错 删除并重新创建 每次运行(参见 overwriteOldOutputDir),所以不要将其指向包含其他文件的目录. 其父目录必须存在 . |
sdkInterfacePath | 请求函数''模块。 它必须输出 BaseRequestArgs 财务报告和审定财务报表 BaseRequestFn 类型; 生成 baseRequest.ts 再出口它们. |
命名
默认情况下, 生成的名称跟随您的控制器 : users.controller.ts 生产 users.controller.ts 导出 UsersController组合成 API.UsersController。 。 。 . controllerOutput 调整这些名称,每个名称都有可选 removeSuffix (从名字的末尾删除,如果在场)和 addSuffix (附录):
| 选项 | 适用于 | 示例 UsersController 内 users.controller.ts |
|---|---|---|
controllerOutput.fileName | 生成的文件名称, 来自控制器的文件名称, 没有扩展名 | { "removeSuffix": ".controller", "addSuffix": ".endpoints" } 给 users.endpoints.ts |
controllerOutput.exportName | 生成的类名 | { "removeSuffix": "Controller", "addSuffix": "Endpoints" } 给 UsersEndpoints |
controllerOutput.endpointGroupName | 键入 groupRTKEndpoints(api) | { "removeSuffix": "Controller" } 给 API.Users |
removeSuffix 被解释为正则表达式 : 如果您需要精确匹配, 请逃脱特殊字符( in) .controller,则 . 与任何字符匹配,这在实践中是无害的).
产出
| 选项 | 默认 | 说明 |
|---|---|---|
prettify | true | 用 Pretier 格式生成文件 |
prettierConfig | - 说吧 | Pretier 配置文件的路径( JSON) 。 默认, 最近的 .prettierrc 上一个文件 sdkOutputPath 如果使用,则只使用JSON .prettierrc 文件被支持. |
generateTimestamps | true | 在每个文件中添加生成日期 。 设定为 false 以在无变化时获得相同的输出(建议). |
overwriteOldOutputDir | true | 替换 sdkOutputPath 如果它存在的话。 何时 false如果存在,则生成失败. |
generateDefaultSdkInterface | true | 在 sdkInterfacePath 如果文件不存在。 已存在的文件永远不会被覆盖 . |
分析
| 选项 | 默认 | 说明 |
|---|---|---|
tsconfigFile | tsconfig.json | API 的 tsconfig 文件名称, 以 apiInputPath (文件名,不是路径) |
engine | tsgo | 分析引擎数字 : tsgo, (中文(简体) ), typescript 或者说 ts-morph |
typescriptPath | - 说吧 | 脚本软件包 typescript 发动机。 默认情况下 typescript 为 API 安装的软件包(从 apiInputPath)为被使用. |
magicTypes | [] | 无法取出的类型、 纯 SDK 模式 |
调试
| 选项 | 默认 | 说明 |
|---|---|---|
verbose | false | 记录每个分析控制器、方法、参数和类型 |
jsonOutput | - 说吧 | 将分析结果(模块、控制器、路线和类型)写入此 JSON 文件 |
jsonPrettyOutput | false | 缩进 JSON 输出 |
noColor | false | 目前无效:设置 FORCE_COLOR=0 禁用颜色的环境变量 |
请求函数
生成的端点本身不会执行请求:每个端点都会将其论据变成一个 BaseRequestArgs 对象并把它交给 RTK 查询的基础查询,这是您的请求功能。 您决定如何提出请求: 基础 URL, 信头, 认证, 序列化, 错误格式, 重试, 日志...
// web/src/api/BaseRequest.ts
import type { BaseQueryFn } from '@reduxjs/toolkit/query'
/** What each generated endpoint asks the request function to do */
export type BaseRequestArgs = {
body: unknown
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
query: Record<string, any>
route: string // e.g. "/users/42", with route parameters already interpolated
}
export type RequestError = { status: number; message: string }
export type BaseRequestFn = BaseQueryFn<BaseRequestArgs, unknown, RequestError>
// Include your API's global prefix and version, if any (e.g. app.setGlobalPrefix('api'))
const BASE_URL = 'https://api.example.com'
export const BaseRequest: BaseRequestFn = async ({ body, method, query, route }) => {
const url = new URL(BASE_URL + route)
for (const [key, value] of Object.entries(query)) {
if (value === undefined) continue
url.searchParams.append(key, typeof value === 'object' && value !== null ? JSON.stringify(value) : String(value))
}
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' /* , Authorization: `Bearer ${token}` */ },
body: method === 'GET' ? undefined : JSON.stringify(body ?? {}),
})
const text = await response.text()
if (!response.ok) {
return { error: { status: response.status, message: text } }
}
return { data: text ? JSON.parse(text) : undefined }
}
该函数接收了RTK Query 通常的额外参数(api,与 getState, (中文(简体) ), dispatch 财务报告和审定财务报表 signal,以及 extraOptions),因此您可以从商店读取一个令牌或支持请求取消. 为全球处理出错( 如显示祝酒词), 将函数包裹并传递给 createApi。 。 。 .
这个 error 由钩子返回是您声明的错误类型 BaseRequestFn,或RTK查询 SerializedError (意外例外).
启动执行. 何时 sdkInterfacePath 不存在, 生成器会写一个更完整的启动器( query 序列化, a RequestError 分类,包含 HTTP 元数据,来自 Nest 错误回复的用户信息),您可以适应。 它假设API是从 /api 在当前主机上.
添加缓存( 塔克)
生成的端点不声明缓存标记. 用RTK 查询器添加 enhanceEndpoints,它保持所有键入:
export const rtkApi = createApi({
reducerPath: 'api',
baseQuery: BaseRequest,
endpoints: endpoints.buildRTKEndpoints,
}).enhanceEndpoints({
addTagTypes: ['Comment'],
endpoints: {
listComments: { providesTags: ['Comment'] },
addComment: { invalidatesTags: ['Comment'] },
removeComment: { invalidatesTags: ['Comment'] },
},
})
export const API = endpoints.groupRTKEndpoints(rtkApi)
其他每端选项也一样 keepUnusedDataFor 或者说 transformResponse。 。 。 .
命令行
nest-sdk-generator <config file> [--generate-rtk | --generate] [--engine <tsgo|typescript|ts-morph>]
| 参数 | 说明 |
|---|---|
<config file> | 配置文件的路径( 需要) |
--generate-rtk | 生成 RTK 查询客户端 |
--generate | 生成 简单 SDK |
--engine <name>, (中文(简体) ), --engine=<name> | 分析引擎, 覆盖配置文件 |
无 --generate-rtk 或者说 --generate,只分析API:对验证它有用,或生成 jsonOutput。进程退出时会有一个关于错误的非零代码(1 用于分析和生成错误, 2 改为 4 当配置文件缺失或无效时.
分析引擎
大部分生成时间由TypeScript编译器分析API所花. 现有三台发动机:
| 发动机 | 编译器 | 页:1 |
|---|---|---|
tsgo (默认) | TypeScript 7 的本地编译器, 捆绑 | 最快 |
typescript | 您的 API 自己的 TypeScript 编译器( 6. x), 正在处理 | 输出与 tsgo*. 如果 tsgo 不能开始. |
ts-morph | TypeScript 4.8, 用 ts- 形态捆绑 | 输出与本套件的 2. x 版本相同 |
在有66个控制器的API上,有687个路由和~2500个源文件(加上很大的依赖性,例如: googleapis),生成RTK查询客户端获取:
| 发动机 | 会期 |
|---|---|
tsgo | 2.4级 |
typescript | 8.8级 |
ts-morph | 17.7s (单位:千美元) |
这个 tsgo 引擎需要Node.js 20.19+或22.12+,而Node.js本身而不是Bun的运行时间: bun x 财务报告和审定财务报表 npx 用节点运行生成器,但 bun --bun 没有。 何时 tsgo 无法启动( 不支持运行时间或平台) , 生成器返回到 typescript 引擎和说。 这个 typescript 引擎需要带有 JavaScript 编译器 API 的 TypeScript 6 (或更古老) 包: API 的自有, 或用 API 的自有 typescriptPath。 。 。 .
较新的类型Script版本比TypeScript 4.8(例如. (User | undefined)[] 变成 User[] 之后 .filter() 有类型守护的, 命令工会成员确定, 并省略默认类型参数。 这个 tsgo 财务报告和审定财务报表 typescript 因此,发动机产生的类型可能略为不同,更精确 ts-morph 发动机.
* TypeScript 6和7打印出少数不同种类的稀有类型(如在通用函数内声明的类,用作基类). 这不影响RTK查询客户端,但可以改变为平版SDK模式所提取的类型并 jsonOutput。 。 。 .
建议的工作流程
-
不要让生成的客户端. 添加
sdkOutputPath给您的.gitignore在任何需要它的东西之前产生它:{ "scripts": { "build:api": "nest-sdk-generator api-gen.config.json --generate-rtk", "dev": "npm run build:api && next dev", "build": "npm run build:api && next build", "typecheck": "npm run build:api && tsc --noEmit", "test": "npm run build:api && vitest run" } } -
开发时再生. 生成器不监视文件;与文件监视器配对,例如.
nodemon --watch ../api/src -e ts --exec "npm run build:api"。 。 。 . -
设定
generateTimestamps: false所以没有改变的 API 生成相同的文件, 和工具看输出 不重建白. -
类型检查 CI 中的客户端 . 由于客户端是从API生成的,一个破碎的API会作为使用它的客户端代码中的类型错误而改变表面.
限制
- 只有静态分析. 路由是从以下所列装饰器中发现的: 控制器如何成为终点: 动态登记路线,
@All(), (中文(简体) ),@Head(), (中文(简体) ),@Options(), (中文(简体) ),@Controller()选项对象、版本和全局前缀不考虑(在请求函数的基址中输入全局前缀). - 无通配符路径 (单位:千美元)
*, (中文(简体) ),+, (中文(简体) ),?在路径中),并且路由参数名称可能只包含字母,数字和_。 。 。 . - 常规
@Param()不支持对象: 使用一个@Param('name')每个路由参数. - 可选的密钥参数需要密钥.
@Query('page') page?: number生产{ page: number | undefined }: 通过page: undefined明确,或使用@Query()具有可选属性的 DTO. - 没有前缀的控制路径 . 与
@Controller()(无前缀),该方法的路径如: 写入@Get('/articles')而不是说@Get('articles')以/。 。 。 . - 每个目录有一个模块文件 . 通过寻找一个
*.module.ts中的文件,然后是其父母. - 独有名相. 控制器类名称和跨控制器的方法名称必须是独一无二的.
- 类型必须在文件的顶层声明 . 在分析过程中还提取出路线所使用的类型(用于平面SDK模式和
jsonOutput)),并且不能在名称空间内声明类型,这导致生成失败. - API没有打字. 生成器不会在 API 中报告编译错误; 编译器无法解析的类型变为
any。 。 。 .
解决问题
| 错误 | 原因和修补 |
|---|---|
Duplicate method name found: X in A and B | 两个控制器有一个方法命名 X,将生成两个同名的RTK查询端点。 重命名其中一个. |
GET request should not have @Body params 编号 : POST request should not have @Query params | 将输入移动到 @Query() (GET) 或 (中文(简体) ) @Body() (其他方法). |
Route param X does not appear in route URL | A 类 @Param('X') 没有匹配 :X 或通用软件 @Param() 对象被使用. |
No module file was found for controller at path ... | 添加一个 *.module.ts 在控制器目录或父目录中的文件. |
Type X was not found in file ... | X 将不会在文件的顶端(例如,它位于命名空间)声明. |
| 检查客户端时输入 API 文件错误 | 客户端的编译器选项与 API 的对齐(参见 客户端类型脚本设置) (中文(简体) ). |
| 变慢了 | 检查一下 tsgo 使用引擎( e)"verbose": true 记录引擎,而且你没有运行 发电机与 bun --bun。 。 。 . |
为了其他一切, "verbose": true 记录分析的每个步骤,以及 jsonOutput 显示分析者对你的API的理解.
普通 SDK 模式 (--generate(中文(简体) )
--generate 生成一个框架不可知 SDK 而不是 RTK 查询端点: 每个控制器有一个文件 queries 财务报告和审定财务报表 mutations 方法称为 request(method, uri, body, query) 函数导出 sdkInterfacePath。在此模式中,路线所使用的类型会被复制成 _types 目录( 来自 API 目录以外的类型) _types/_externalN/...),因此客户端不需要访问API的来源.
无法复制的类型,例如带有循环引用或许多依赖关系的ORM类型,可以用占位符替换 magicTypes数字 :
{
"magicTypes": [
{
"nodeModuleFilePath": "@mikro-orm/core/entity/Collection.d.ts",
"typeName": "Collection",
"placeholderContent": "export type Collection<T, _> = Array<T>;"
}
]
}
当一个类型命名 typeName 从 node_modules/<nodeModuleFilePath>, (中文(简体) ), placeholderContent 改为使用。 米克罗 -ORM的 Collection 被默认替换.
此模式为相容性所维护:RTK查询客户端是本套件的主要用例. 已知问题:具有索引签名的类([key: string]: ...)生成类型文件 Pretier 拒绝.
从 2.x 升级
3.0版本添加 分析引擎; 配置和生成的文件结构不变.
- 生成速度快得多,但默认
tsgo发动机需要20.19+ / 22.12+. - 一些生成的类型可能会改变,因为更新的TypeScript版本更准确地推断类型. 如果这打破了您的构建, 请修补客户端代码( 新类型反映了您的 API 编译器所看到的), 或临时使用
--engine=ts-morph以获得精确的 2.x 输出. jsonOutput再次工作(在 2.x 中坠毁).
发展
npm install
npm run build # Build the generator into build/
npm run build:baseline # Build version 2.3.1 into .parity/baseline, used as a reference by the tests
npm test # Run the tests
npm run lint
测试生成客户端用于固定 API in test/fixtures (和~30个出错案例)并比较以下结果:
- 基准和基准
ts-morph引擎,它必须输出完全相同的输出 - 后面的分析器
tsgo财务报告和审定财务报表typescript引擎,运行在 ts-morph 的 TypeScript 4.8 程序上,它也必须输出完全相同的输出(这显示它相当于原始分析器) - 页:1
tsgo财务报告和审定财务报表typescript引擎,必须输出完全相同的输出
还要在真正的项目上运行 PARITY_REAL_API_CONFIG 以到生成器配置文件的路径;生成的文件被写入临时目录.
信贷和许可证
此软件包由 构建视野开始于 巢- sdk- 生成器 以 孤独石,它生成了平版SDK;这个叉增加了RTK查询客户端和分析引擎.
现按 MIT 许可证。 。 。 .