Next.js ConnectRPC 代理模式
发布: (2026年2月4日 GMT+8 22:05)
4 min read
原文: Dev.to
Source: Dev.to

1. 依赖
为 ConnectRPC v2、Next.js 15 和 TanStack Query 所需的包。
package.json
{
"name": "airborneo-grpc-proxy",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint",
"generate": "buf generate proto"
},
"dependencies": {
"@bufbuild/protobuf": "^2.2.3",
"@connectrpc/connect": "^2.0.1",
"@connectrpc/connect-next": "^2.0.1",
"@connectrpc/connect-node": "^2.1.1",
"@connectrpc/connect-query": "^2.0.1",
"@connectrpc/connect-web": "^2.1.1",
"@tanstack/react-query": "^5.0.0",
"next": "15.1.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@bufbuild/buf": "^1.47.0",
"@bufbuild/protoc-gen-es": "^2.2.3",
"@connectrpc/protoc-gen-connect-query": "^2.0.1",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"typescript": "^5"
}
}
2. Proto 定义与生成
我们定义了两个 proto 文件。GreetService 导入 ElizaService 的消息,以确保代理与上游后端之间的类型严格一致。
proto/connectrpc/eliza/v1/eliza.proto
syntax = "proto3";
package connectrpc.eliza.v1;
message SayRequest {
string sentence = 1;
}
message SayResponse {
string sentence = 1;
}
service ElizaService {
rpc Say(SayRequest) returns (SayResponse) {}
}
proto/greet/v1/greet.proto
syntax = "proto3";
package greet.v1;
import "connectrpc/eliza/v1/eliza.proto";
// This service mimics the remote service but is exposed locally
service GreetService {
rpc Say(connectrpc.eliza.v1.SayRequest) returns (connectrpc.eliza.v1.SayResponse) {}
}
buf.gen.yaml
version: v1
plugins:
# 1. Generates Types AND Service Definitions (v2 Standard)
- plugin: es
path: ./node_modules/.bin/protoc-gen-es
out: gen
opt: target=ts
# 2. Generates React Hooks (TanStack Query)
- plugin: connect-query
path: ./node_modules/.bin/protoc-gen-connect-query
out: gen
opt: target=ts
运行生成器:
npm run generate
3. 代理工具
此工具通过将方法调用转发到目标客户端,动态实现服务定义。它让我们能够在本地 API 路由上“挂载”远程后端,而无需手动重写每个解析器。
src/utils/proxy.ts
import { type DescService } from "@bufbuild/protobuf";
import { type Client } from "@connectrpc/connect";
export function createProxy(
service: T,
client: Client,
) {
const implementation: any = {};
for (const method of service.methods) {
const fnName = method.localName;
implementation[fnName] = async (req: any) => {
console.log(`[Proxy] Forwarding ${fnName} for ${service.typeName}`);
// Call the client dynamically
return await (client as any)[fnName](req);
};
}
console.log(
`[Proxy] Built methods for ${service.typeName}:`,
Object.keys(implementation),
);
return implementation;
}
4. API 路由实现
我们创建一个指向演示 Eliza 服务的后端连接,然后将本地的 GreetService 路由到代理该连接。
pages/api/[[...connect]].ts
import { nextJsApiRouter } from "@connectrpc/connect-next";
import { ConnectRouter } from "@connectrpc/connect";
import { createClient } from "@connectrpc/connect";
import { createConnectTransport } from "@connectrpc/connect-node";
// Generated Code
import { GreetService } from "@/gen/greet/v1/greet_pb";
import { ElizaService } from "@/gen/connectrpc/eliza/v1/eliza_pb";
import { createProxy } from "@/src/utils/proxy";
// 1. The Backend Connection (Target)
const elizaBackend = createClient(
ElizaService,
createConnectTransport({
baseUrl: "https://demo.connectrpc.com",
httpVersion: "1.1",
}),
);
function routes(router: ConnectRouter) {
// ---------------------------------------------------------
// ROUTE 1: GreetService (Proxy -> ElizaService)
// URL: /api/greet.v1.GreetService/Say
// ---------------------------------------------------------
router.service(GreetService, createProxy(GreetService, elizaBackend));
}
export const { handler, config } = nextJsApiRouter({ routes });
export default handler;
5. 客户端使用
前端使用 connect-web 与本地的 Next.js API 通信。(原文接下来提供了示例客户端代码;如有需要可在此处加入。)
app/page.tsx
"use client";
import { useState } from "react";
import { useMutation } from "@tanstack/react-query";
import { createClient } from "@connectrpc/connect";
import { createConnectTransport } from "@connectrpc/connect-web";
import { GreetService } from "@/gen/greet/v1/greet_pb";
// Point to YOUR local API
const transport = createConnectTransport({ baseUrl: "/api" });
const client = createClient(GreetService, transport);
export default function ChatPage() {
const [input, setInput] = useState("");
const [history, setHistory] = useState([]);
const mutation = useMutation({
mutationFn: async (msg: string) => {
// Input is typed as SayRequest (from Eliza)
const res = await client.say({ sentence: msg });
// Output is typed as SayResponse (from Eliza)
return res.sentence;
},
onSuccess: (answer) => {
setHistory((prev) => [...prev, `Eliza (via Proxy): ${answer}`]);
},
});
return (
代理聊天
setInput(e.target.value)}
placeholder="Say something..."
style={{ padding: "8px", flexGrow: 1 }}
/>
mutation.mutate(input)}
disabled={mutation.isPending}
>
{mutation.isPending ? "Sending..." : "Send"}
{history.map((line, i) => (
{line}
))}
);
} 