From ace80a2f95627eb7ce863aeff5c4de267098e651 Mon Sep 17 00:00:00 2001 From: imbytecat Date: Sun, 18 Jan 2026 00:47:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0RPC=20API=E8=B7=AF?= =?UTF-8?q?=E7=94=B1=E6=94=AF=E6=8C=81=E4=B8=8EZod=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E5=93=8D=E5=BA=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加RPC API路由处理,支持多种HTTP方法并集成Zod验证错误的自定义错误响应。 - 添加对 `/api/rpc/$` 路由路径的支持,包括路由配置、类型定义和路由树的更新。 --- src/routeTree.gen.ts | 24 ++++++++++++-- src/routes/api/rpc.$.ts | 70 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 src/routes/api/rpc.$.ts diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 2be49ea..03a4520 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -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) diff --git a/src/routes/api/rpc.$.ts b/src/routes/api/rpc.$.ts new file mode 100644 index 0000000..18fa6f9 --- /dev/null +++ b/src/routes/api/rpc.$.ts @@ -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, + }, + }, +})