feat: 添加RPC API路由支持与Zod验证错误响应

- 添加RPC API路由处理,支持多种HTTP方法并集成Zod验证错误的自定义错误响应。
- 添加对 `/api/rpc/$` 路由路径的支持,包括路由配置、类型定义和路由树的更新。
This commit is contained in:
2026-01-18 00:47:45 +08:00
parent 7750b11fc7
commit ace80a2f95
2 changed files with 91 additions and 3 deletions

View File

@@ -11,6 +11,7 @@
import { Route as rootRouteImport } from './routes/__root'
import { Route as TodoRouteImport } from './routes/todo'
import { Route as IndexRouteImport } from './routes/index'
import { Route as ApiRpcSplatRouteImport } from './routes/api/rpc.$'
const TodoRoute = TodoRouteImport.update({
id: '/todo',
@@ -22,31 +23,40 @@ const IndexRoute = IndexRouteImport.update({
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const ApiRpcSplatRoute = ApiRpcSplatRouteImport.update({
id: '/api/rpc/$',
path: '/api/rpc/$',
getParentRoute: () => rootRouteImport,
} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/todo': typeof TodoRoute
'/api/rpc/$': typeof ApiRpcSplatRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/todo': typeof TodoRoute
'/api/rpc/$': typeof ApiRpcSplatRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/todo': typeof TodoRoute
'/api/rpc/$': typeof ApiRpcSplatRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/todo'
fullPaths: '/' | '/todo' | '/api/rpc/$'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/todo'
id: '__root__' | '/' | '/todo'
to: '/' | '/todo' | '/api/rpc/$'
id: '__root__' | '/' | '/todo' | '/api/rpc/$'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
TodoRoute: typeof TodoRoute
ApiRpcSplatRoute: typeof ApiRpcSplatRoute
}
declare module '@tanstack/react-router' {
@@ -65,12 +75,20 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/api/rpc/$': {
id: '/api/rpc/$'
path: '/api/rpc/$'
fullPath: '/api/rpc/$'
preLoaderRoute: typeof ApiRpcSplatRouteImport
parentRoute: typeof rootRouteImport
}
}
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
TodoRoute: TodoRoute,
ApiRpcSplatRoute: ApiRpcSplatRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)

70
src/routes/api/rpc.$.ts Normal file
View File

@@ -0,0 +1,70 @@
import { ORPCError, onError, ValidationError } from '@orpc/server'
import { RPCHandler } from '@orpc/server/fetch'
import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'
import { router } from '@/orpc/router'
const handler = new RPCHandler(router, {
interceptors: [
onError((error) => {
console.error(error)
}),
],
clientInterceptors: [
onError((error) => {
if (
error instanceof ORPCError &&
error.code === 'BAD_REQUEST' &&
error.cause instanceof ValidationError
) {
// If you only use Zod you can safely cast to ZodIssue[]
const zodError = new z.ZodError(
error.cause.issues as z.core.$ZodIssue[],
)
throw new ORPCError('INPUT_VALIDATION_FAILED', {
status: 422,
message: z.prettifyError(zodError),
data: z.flattenError(zodError),
cause: error.cause,
})
}
if (
error instanceof ORPCError &&
error.code === 'INTERNAL_SERVER_ERROR' &&
error.cause instanceof ValidationError
) {
throw new ORPCError('OUTPUT_VALIDATION_FAILED', {
cause: error.cause,
})
}
}),
],
})
async function handle({ request }: { request: Request }) {
const { matched, response } = await handler.handle(request, {
prefix: '/api/rpc',
context: {},
})
if (matched) {
return response
}
return new Response('Not Found', { status: 404 })
}
export const Route = createFileRoute('/api/rpc/$')({
server: {
handlers: {
HEAD: handle,
GET: handle,
POST: handle,
PUT: handle,
PATCH: handle,
DELETE: handle,
},
},
})