tRPC 11 client + server over fetch with superjson (Vitest): working versions (tested 2026-10-15)
End-to-end typed tRPC 11: fetch adapter server, createTRPCClient with httpBatchLink and superjson transformer for Dates and Maps, tested in-process with Vitest.
passed3 passed · 0 failed2026-10-15 11:57 UTC- Status
- passed
- Last passed
- 2026-10-15 11:57 UTC
- Tests
- 3 / 3
- Language
- TypeScript
- Runtime
- node v22.23.3
npm install --no-audit --no-fund --ignore-scripts=false npm test
node v22.23.3 linux 6.17.0-1022-azure x64 runner: github-actions 38730348195
The code
Exactly the files that passed. Your agent can pull them with get_recipe("trpc-client-server-superjson") (install), or see them on GitHub.
{
"name": "trpc-client-server-superjson",
"private": true,
"type": "module",
"scripts": {
"test": "vitest run --reporter=default --reporter=json --outputFile=test-report.json"
},
"dependencies": {
"@trpc/server": "11.19.0",
"@trpc/client": "11.19.0",
"superjson": "2.2.6",
"zod": "4.6.7"
},
"devDependencies": {
"@types/node": "24.19.2",
"vitest": "5.0.4"
}
}
import { initTRPC } from "@trpc/server";
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import superjson from "superjson";
import { z } from "zod";
const t = initTRPC.create({ transformer: superjson });
const events: { name: string; at: Date }[] = [];
export const appRouter = t.router({
addEvent: t.procedure.input(z.object({ name: z.string().min(1), at: z.date() })).mutation(({ input }) => {
events.push(input);
return { count: events.length };
}),
nextEvent: t.procedure.query(() => events.toSorted((a, b) => a.at.getTime() - b.at.getTime())[0] ?? null),
tally: t.procedure.query(() => new Map(events.map((e) => [e.name, e.at]))),
});
export type AppRouter = typeof appRouter;
export const handler = (req: Request) => fetchRequestHandler({ endpoint: "/trpc", req, router: appRouter, createContext: () => ({}) });
import { createTRPCClient, httpBatchLink } from "@trpc/client";
import superjson from "superjson";
import type { AppRouter } from "./server";
// transformer goes on the link in tRPC 11 (not on createTRPCClient)
export const makeClient = (url: string, fetchImpl?: typeof fetch) =>
createTRPCClient<AppRouter>({ links: [httpBatchLink({ url, transformer: superjson, fetch: fetchImpl })] });
import { describe, it, expect, vi } from "vitest";
import { TRPCClientError } from "@trpc/client";
import { handler } from "../src/server";
import { makeClient } from "../src/client";
const fetchSpy = vi.fn((input: RequestInfo | URL, init?: RequestInit) => handler(new Request(input, init)));
const client = makeClient("http://local/trpc", fetchSpy as unknown as typeof fetch);
describe("trpc over fetch", () => {
it("round-trips Dates and Maps", async () => {
const at = new Date("2026-11-01T10:00:00Z");
await client.addEvent.mutate({ name: "launch", at });
const next = await client.nextEvent.query();
expect(next?.at).toBeInstanceOf(Date);
expect(next?.at.toISOString()).toBe(at.toISOString());
const tally = await client.tally.query();
expect(tally).toBeInstanceOf(Map);
expect(tally.get("launch")).toBeInstanceOf(Date);
});
it("batches parallel calls into one request", async () => {
fetchSpy.mockClear();
await Promise.all([client.nextEvent.query(), client.tally.query()]);
expect(fetchSpy).toHaveBeenCalledTimes(1);
});
it("returns typed validation errors", async () => {
const err = await client.addEvent.mutate({ name: "", at: new Date() }).catch((e) => e);
expect(err).toBeInstanceOf(TRPCClientError);
expect(err.data?.code).toBe("BAD_REQUEST");
});
});
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.
Related stacks
Hono + Drizzle ORM + SQLite REST API (Vitest)
CRUD REST API in Hono backed by Drizzle ORM on better-sqlite3, with Zod validation, tested with Vitest and app.request().
SvelteKit form actions with fail() and Zod, unit-tested (Vitest)
SvelteKit +page.server.ts form actions that validate with Zod, return fail(400) with field errors, and redirect on success, tested by calling the actions with a fake RequestEvent.
tRPC 11 context, auth middleware and createCaller tests
tRPC 11 router with a request context, a protectedProcedure built from middleware, Zod inputs and TRPCError codes, tested with createCaller (no HTTP).
Anthropic TypeScript SDK tool use loop, tested with a mocked fetch
Run a Claude tool_use loop with the official Anthropic SDK and Zod-validated tool inputs, tested offline with an injected fetch.
All @trpc/server recipesAll @trpc/client recipesAll superjson recipesAll Zod recipesAll vitest recipes