本指令基于通用CRUD规范,指导如何为已存在的通用CRUD路由添加自定义扩展路由,采用模块化方式保持代码清晰。
当通用CRUD提供的标准路由(GET /, POST /, GET /{id}, PUT /{id}, DELETE /{id})无法满足业务需求时,需要添加自定义业务路由。
找到对应的通用CRUD路由文件,通常位于:
src/server/api/[实体名]/index.ts为每个扩展功能创建单独的路由文件:
src/server/api/your-entity/
├── index.ts # 聚合路由(已存在)
├── batch/ # 新增 - 批量操作
│ └── delete.ts # 批量删除
├── [id]/ # 新增 - 单条记录扩展操作
│ ├── status.ts # 状态更新
│ ├── toggle.ts # 状态切换
│ └── audit.ts # 审核操作
├── export.ts # 新增 - 数据导出
├── import.ts # 新增 - 数据导入
├── stats.ts # 新增 - 统计信息
└── upload.ts # 新增 - 文件上传
batch/delete.tsimport { createRoute, OpenAPIHono } from '@hono/zod-openapi';
import { z } from '@hono/zod-openapi';
import { ErrorSchema } from '@/server/utils/errorHandler';
import { AppDataSource } from '@/server/data-source';
import { YourEntityService } from '@/server/modules/your-module/your-entity.service';
import { AuthContext } from '@/server/types/context';
import { authMiddleware } from '@/server/middleware/auth.middleware';
const routeDef = createRoute({
method: 'delete',
path: '/',
middleware: [authMiddleware],
request: {
body: {
content: {
'application/json': {
schema: z.object({
ids: z.array(z.number().int().positive()).openapi({
description: '要删除的ID列表',
example: [1, 2, 3]
})
})
}
}
}
},
responses: {
200: {
description: '批量删除成功',
content: {
'application/json': {
schema: z.object({
deletedCount: z.number().openapi({ example: 3, description: '删除的记录数' })
})
}
}
},
400: { description: '请求参数错误', content: { 'application/json': { schema: ErrorSchema } } },
500: { description: '服务器错误', content: { 'application/json': { schema: ErrorSchema } } }
}
});
const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
try {
const { ids } = await c.req.json();
const service = new YourEntityService(AppDataSource);
let deletedCount = 0;
for (const id of ids) {
const result = await service.delete(id);
if (result) deletedCount++;
}
return c.json({ deletedCount }, 200);
} catch (error) {
return c.json({ code: 500, message: '批量删除失败' }, 500);
}
});
export default app;
[id]/status.tsimport { createRoute, OpenAPIHono } from '@hono/zod-openapi';
import { z } from '@hono/zod-openapi';
import { YourEntitySchema } from '@/server/modules/your-module/your-entity.schema';
import { parseWithAwait } from '@/server/utils/parseWithAwait';
import { ErrorSchema } from '@/server/utils/errorHandler';
import { AppDataSource } from '@/server/data-source';
import { YourEntityService } from '@/server/modules/your-module/your-entity.service';
import { AuthContext } from '@/server/types/context';
import { authMiddleware } from '@/server/middleware/auth.middleware';
const routeDef = createRoute({
method: 'patch',
path: '/',
middleware: [authMiddleware],
request: {
params: z.object({
id: z.string().openapi({
param: { name: 'id', in: 'path' },
example: '1',
description: '记录ID'
})
}),
body: {
content: {
'application/json': {
schema: z.object({
status: z.number().openapi({ example: 1, description: '新状态值' })
})
}
}
}
},
responses: {
200: { description: '状态更新成功', content: { 'application/json': { schema: YourEntitySchema } } },
404: { description: '记录不存在', content: { 'application/json': { schema: ErrorSchema } } },
500: { description: '服务器错误', content: { 'application/json': { schema: ErrorSchema } } }
}
});
const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
try {
const { id } = c.req.valid('param');
const { status } = await c.req.json();
const service = new YourEntityService(AppDataSource);
const result = await service.update(Number(id), { status });
if (!result) {
return c.json({ code: 404, message: '记录不存在' }, 404);
}
// 使用 parseWithAwait 处理响应数据
const validatedResult = await parseWithAwait(YourEntitySchema, result);
return c.json(validatedResult, 200);
} catch (error) {
return c.json({ code: 500, message: '状态更新失败' }, 500);
}
});
export default app;
export.tsimport { createRoute, OpenAPIHono } from '@hono/zod-openapi';
import { z } from '@hono/zod-openapi';
import { ErrorSchema } from '@/server/utils/errorHandler';
import { AppDataSource } from '@/server/data-source';
import { YourEntityService } from '@/server/modules/your-module/your-entity.service';
import { YourEntitySchema } from '@/server/modules/your-module/your-entity.schema';
import { parseWithAwait } from '@/server/utils/parseWithAwait';
import { AuthContext } from '@/server/types/context';
import { authMiddleware } from '@/server/middleware/auth.middleware';
const routeDef = createRoute({
method: 'get',
path: '/',
middleware: [authMiddleware],
request: {
query: z.object({
format: z.enum(['csv', 'xlsx']).default('csv').openapi({
description: '导出格式',
example: 'csv'
}),
keyword: z.string().optional().openapi({
description: '搜索关键词',
example: '测试'
}),
filters: z.string().optional().openapi({
description: '筛选条件(JSON字符串)',
example: '{"status":1}'
})
})
},
responses: {
200: {
description: '导出文件',
content: {
'text/csv': { schema: z.string() },
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': { schema: z.any() }
}
}
}
});
const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
try {
const { format, keyword, filters } = c.req.valid('query');
const service = new YourEntityService(AppDataSource);
let filterObj = {};
if (filters) {
try {
filterObj = JSON.parse(filters);
} catch (e) {
return c.json({ code: 400, message: '筛选条件格式错误' }, 400);
}
}
const [data] = await service.getList(1, 1000, keyword, undefined, filterObj);
// 使用 parseWithAwait 处理数据格式
const validatedData = await parseWithAwait(z.array(YourEntitySchema), data);
if (format === 'csv') {
const csv = convertToCSV(validatedData);
return new Response(csv, {
headers: {
'Content-Type': 'text/csv',
'Content-Disposition': 'attachment; filename="export.csv"'
}
});
}
return c.json({ code: 400, message: '不支持的导出格式' }, 400);
} catch (error) {
return c.json({ code: 500, message: '导出失败' }, 500);
}
});
export default app;
function convertToCSV(data: any[]): string {
if (!data || data.length === 0) return '';
const headers = Object.keys(data[0]);
const csvHeaders = headers.join(',');
const csvRows = data.map(row =>
headers.map(header => {
const value = row[header];
return typeof value === 'string' && value.includes(',') ? `"${value}"` : value;
}).join(',')
);
return [csvHeaders, ...csvRows].join('\n');
}
在 index.ts 中聚合基础CRUD路由和所有扩展路由:
// src/server/api/your-entity/index.ts
import { OpenAPIHono } from '@hono/zod-openapi';
import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
import { YourEntity } from '@/server/modules/your-module/your-entity.entity';
import { YourEntitySchema, CreateYourEntityDto, UpdateYourEntityDto } from '@/server/modules/your-module/your-entity.schema';
import { authMiddleware } from '@/server/middleware/auth.middleware';
// 导入基础路由和各扩展路由
import batchDeleteRoute from './batch/delete';
import statusUpdateRoute from './[id]/status';
import exportRoute from './export';
// 1. 创建基础CRUD路由
const yourEntityRoutes = createCrudRoutes({
entity: YourEntity,
createSchema: CreateYourEntityDto,
updateSchema: UpdateYourEntityDto,
getSchema: YourEntitySchema,
listSchema: YourEntitySchema,
searchFields: ['name', 'description'],
middleware: [authMiddleware]
});
// 2. 聚合所有路由(保持链式)
const app = new OpenAPIHono()
.route('/batch', batchDeleteRoute) // 批量操作路由
.route('/:id/status', statusUpdateRoute) // 状态更新路由
.route('/export', exportRoute) // 导出路由
.route('/', yourEntityRoutes); // 基础CRUD路由必需放最后,不然自定义路由会覆盖掉基础CRUD路由
// 3. 导出聚合后的路由
export default app;
DELETE /your-entity/batchPATCH /your-entity/batch/statusPOST /your-entity/importGET /your-entity/statsGET /your-entity/chart-dataPOST /your-entity/uploadGET /your-entity/download/{id}GET /your-entity/exportPATCH /your-entity/{id}/toggle-statusPOST /your-entity/{id}/auditGET /your-entity/{id}/related-dataPUT /your-entity/{id}/relations/batch, /import, /export/status, /toggle所有扩展路由的 Zod Schema 定义必须遵循以下文件位置规范:
src/server/modules/[模块名]/
├── [实体名].entity.ts # 实体定义
├── [实体名].schema.ts # 实体Schema定义(已存在)
└── schemas/ # 扩展路由专用Schema目录(新增)
├── batch/ # 批量操作Schema
│ └── delete.schema.ts
├── [id]/ # 单条记录操作Schema
│ ├── status.schema.ts
│ ├── toggle.schema.ts
│ └── audit.schema.ts
├── export.schema.ts # 导出操作Schema
├── import.schema.ts # 导入操作Schema
└── stats.schema.ts # 统计操作Schema
[操作名].schema.tsschemas/batch/delete.schema.tsimport { z } from '@hono/zod-openapi';
// 请求Schema
export const BatchDeleteRequestSchema = z.object({
ids: z.array(z.number().int().positive()).openapi({
description: '要删除的ID列表',
example: [1, 2, 3]
})
});
// 响应Schema
export const BatchDeleteResponseSchema = z.object({
deletedCount: z.number().openapi({
example: 3,
description: '删除的记录数'
})
});
// 类型定义
export type BatchDeleteRequest = z.infer<typeof BatchDeleteRequestSchema>;
export type BatchDeleteResponse = z.infer<typeof BatchDeleteResponseSchema>;
schemas/[id]/status.schema.tsimport { z } from '@hono/zod-openapi';
import { YourEntitySchema } from '../../your-entity.schema';
// 路径参数Schema
export const StatusUpdateParamsSchema = z.object({
id: z.string().openapi({
param: { name: 'id', in: 'path' },
example: '1',
description: '记录ID'
})
});
// 请求体Schema
export const StatusUpdateBodySchema = z.object({
status: z.number().openapi({
example: 1,
description: '新状态值'
})
});
// 响应Schema(复用实体Schema)
export const StatusUpdateResponseSchema = YourEntitySchema;
// 类型定义
export type StatusUpdateParams = z.infer<typeof StatusUpdateParamsSchema>;
export type StatusUpdateBody = z.infer<typeof StatusUpdateBodySchema>;
export type StatusUpdateResponse = z.infer<typeof StatusUpdateResponseSchema>;
// src/server/api/your-entity/batch/delete.ts
import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
import { BatchDeleteRequestSchema, BatchDeleteResponseSchema } from '@/server/modules/your-module/schemas/batch/delete.schema';
// ...其他导入
parseWithAwait 处理响应数据,确保类型安全parseWithAwait 是通用CRUD模块提供的数据验证工具,用于确保返回数据的类型安全,支持异步验证和转换。
所有涉及数据查询和返回的扩展路由都应使用 parseWithAwait 处理响应数据。
import { parseWithAwait } from '@/server/utils/parseWithAwait';
// 验证单个实体
const validatedEntity = await parseWithAwait(YourEntitySchema, entityData);
// 验证实体数组
const validatedEntities = await parseWithAwait(z.array(YourEntitySchema), entitiesData);
// 在扩展路由中使用
const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
try {
const data = await yourService.getList();
// 使用 parseWithAwait 确保数据格式正确
const validatedData = await parseWithAwait(z.array(YourEntitySchema), data);
return c.json({
data: validatedData,
pagination: { total, current: page, pageSize }
}, 200);
} catch (error) {
return c.json({ code: 500, message: '获取数据失败' }, 500);
}
});
parseWithAwaitz.array(EntitySchema) 格式验证数组