trpc
Generic tRPC implementation guide. Works with any framework (Next.js, Express, Fastify, Hono, Bun) and any package manager (pnpm, npm, yarn, bun).
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install dimitrigilbert-ai-skills-trpc
Repository
Skill path: trpc
Generic tRPC implementation guide. Works with any framework (Next.js, Express, Fastify, Hono, Bun) and any package manager (pnpm, npm, yarn, bun).
Open repositoryBest for
Primary workflow: Ship Full Stack.
Technical facets: Full Stack, Frontend.
Target audience: everyone.
License: Unknown.
Original source
Catalog source: SkillHub Club.
Repository owner: dimitrigilbert.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install trpc into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/dimitrigilbert/ai-skills before adding trpc to shared team environments
- Use trpc for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: trpc
description: Generic tRPC implementation guide. Works with any framework (Next.js, Express, Fastify, Hono, Bun) and any package manager (pnpm, npm, yarn, bun).
---
# tRPC Quick Start
## Backend Router
```typescript
import { initTRPC, TRPCError } from "@trpc/server";
import { z } from "zod";
export const t = initTRPC.create();
export const router = t.router;
export const publicProcedure = t.procedure;
export const myRouter = router({
getItem: publicProcedure
.input(z.object({ id: z.string() }))
.query(async ({ input }) => {
return await getItem(input.id);
}),
updateItem: publicProcedure
.input(z.object({ id: z.string(), data: z.any() }))
.mutation(async ({ input }) => {
return await updateItem(input.id, input.data);
}),
});
export const appRouter = router({
healthCheck: publicProcedure.query(() => "OK"),
my: myRouter,
});
export type AppRouter = typeof appRouter;
```
## Frontend Client
```typescript
import { createTRPCClient, httpBatchLink } from "@trpc/client";
import { createTRPCOptionsProxy } from "@trpc/tanstack-react-query";
import type { AppRouter } from "./path/to/router";
import { QueryClient } from "@tanstack/react-query";
export const queryClient = new QueryClient();
export const trpcClient = createTRPCClient<AppRouter>({
links: [httpBatchLink({ url: "/api/trpc" })],
});
export const trpc = createTRPCOptionsProxy<AppRouter>({
client: trpcClient,
queryClient,
});
```
## Usage
```typescript
const { data } = useQuery(trpc.my.getItem.queryOptions({ id }));
const mutation = useMutation(trpc.my.updateItem.mutationOptions());
```
## Error Handling
```typescript
throw new TRPCError({
code: "NOT_FOUND",
message: "Resource not found",
});
```
## Advanced
- **Subscriptions**: See [SUBSCRIPTIONS.md](references/SUBSCRIPTIONS.md)
- **Server Adapters**: See [ADAPTERS.md](references/ADAPTERS.md)
- **Context**: See [CONTEXT.md](references/CONTEXT.md)
- **Router Merging**: See [ROUTERS.md](references/ROUTERS.md)
- **Middleware**: See [MIDDLEWARE.md](references/MIDDLEWARE.md)
- **Frontend Patterns**: See [FRONTEND.md](references/FRONTEND.md)
- **Client Links**: See [LINKS.md](references/LINKS.md)
- **Schemas**: See [SCHEMAS.md](references/SCHEMAS.md)
- **Client Errors**: See [CLIENT_ERRORS.md](references/CLIENT_ERRORS.md)
- **Install**: See [INSTALL.md](references/INSTALL.md)
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### references/SUBSCRIPTIONS.md
```markdown
# Subscriptions
## Pattern
Async generator with AbortSignal:
```typescript
onEvent: publicProcedure
.input(z.object({ id: z.string().optional() }))
.subscription(async function* (opts) {
const controller = new AbortController();
const signal = controller.signal;
if (opts.signal) {
opts.signal.addEventListener('abort', () => controller.abort());
}
const queue: Event[] = [];
const unsubscribe = subscribeToEvents((event) => queue.push(event));
try {
while (!signal.aborted) {
if (queue.length > 0) {
const event = queue.shift();
if (event) yield event;
}
await new Promise(resolve => setTimeout(resolve, 10));
}
} finally {
unsubscribe();
}
}),
```
## Key Points
- `opts.signal` is client abort signal
- Create local AbortController for cleanup
- Always call unsubscribe in finally block
- Use queue to buffer events
```
### references/ADAPTERS.md
```markdown
# Server Adapters
## Next.js
```typescript
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { appRouter } from "./router";
import { createContext } from "./context";
export default (req: Request) =>
fetchRequestHandler({
endpoint: "/api/trpc",
req,
router: appRouter,
createContext,
});
```
## Express
```typescript
import * as trpcExpress from "@trpc/server/adapters/express";
import { appRouter } from "./router";
import { createContext } from "./context";
app.use(
"/trpc",
trpcExpress.createExpressMiddleware({
router: appRouter,
createContext,
}),
);
```
## Fastify
```typescript
import fastifyTRPCPlugin from "@trpc/server/adapters/fastify";
import { appRouter } from "./router";
import { createContext } from "./context";
app.register(fastifyTRPCPlugin, {
prefix: "/trpc",
trpcOptions: { router: appRouter, createContext },
});
```
## Hono
```typescript
import { trpcServer } from "@hono/trpc-server";
import { appRouter } from "./router";
import { createContext } from "./context";
app.use("/api/trpc/*", trpcServer({
router: appRouter,
createContext: (_opts, context) => createContext({ context }),
}));
```
## Bun
```typescript
import { fetchRequestHandler } from "@trpc/server/adapters/bun";
import { appRouter } from "./router";
import { createContext } from "./context";
Bun.serve({
async fetch(req) {
return fetchRequestHandler({
endpoint: "/api/trpc",
req,
router: appRouter,
createContext,
});
},
});
```
```
### references/CONTEXT.md
```markdown
# Context
## Pattern
Context provides request-specific data (user, session, db connection):
```typescript
export type CreateContextOptions = {
req?: Request;
res?: Response;
};
export async function createContext({ req }: CreateContextOptions) {
const user = await getUserFromRequest(req);
return {
user,
session: user?.session ?? null,
};
}
export type Context = Awaited<ReturnType<typeof createContext>>;
```
## Initialization with Context
```typescript
import { initTRPC, TRPCError } from "@trpc/server";
import type { Context } from "./context";
export const t = initTRPC.context<Context>().create();
export const router = t.router;
export const publicProcedure = t.procedure;
// Protected procedure using context
export const protectedProcedure = publicProcedure.use(async (opts) => {
if (!opts.ctx.user) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Not authenticated" });
}
return opts.next();
});
```
## Usage in Procedures
```typescript
getUser: protectedProcedure.query(async ({ ctx }) => {
return ctx.user; // Context provides user
});
```
```
### references/ROUTERS.md
```markdown
# Router Merging
## Multiple Routers
```typescript
// features/users/router.ts
export const usersRouter = router({
list: publicProcedure.query(async () => db.users.findMany()),
});
// features/posts/router.ts
export const postsRouter = router({
list: publicProcedure.query(async () => db.posts.findMany()),
});
// routers/index.ts
export const appRouter = router({
healthCheck: publicProcedure.query(() => "OK"),
users: usersRouter,
posts: postsRouter,
});
export type AppRouter = typeof appRouter;
```
## Type Export (Monorepo)
```json
// package.json
{
"exports": {
".": { "types": "./src/index.ts", "default": "./src/index.ts" },
"./router": { "types": "./src/routers/index.ts", "default": "./src/routers/index.ts" }
}
}
```
## Frontend Import
```typescript
import type { AppRouter } from "@my-app/api";
import { createTRPCClient, httpBatchLink } from "@trpc/client";
const client = createTRPCClient<AppRouter>({ /* ... */ });
```
```
### references/MIDDLEWARE.md
```markdown
# Middleware
## Protected Procedure
```typescript
export const protectedProcedure = publicProcedure.use(async (opts) => {
const user = await getCurrentUser(opts.ctx);
if (!user) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Not authenticated" });
}
return opts.next({ ctx: { ...opts.ctx, user } });
});
```
## Rate Limiting
```typescript
export const rateLimited = publicProcedure.use(async (opts) => {
const allowed = await checkRateLimit(opts.ctx);
if (!allowed) {
throw new TRPCError({ code: "TOO_MANY_REQUESTS", message: "Rate limit exceeded" });
}
return opts.next();
});
```
## Logging
```typescript
export const withLogging = publicProcedure.use(async (opts) => {
console.log(`[${opts.type}] ${opts.path}`, opts.input);
const result = await opts.next();
console.log(`[${opts.type}] ${opts.path}: ${Date.now() - start}ms`);
return result;
});
```
## Usage
```typescript
export const adminRouter = router({
deleteUser: protectedProcedure
.use(isAdmin)
.input(z.object({ id: z.string() }))
.mutation(async ({ input }) => await deleteUser(input.id)),
});
```
```
### references/FRONTEND.md
```markdown
# Frontend Patterns
## Query with States
```typescript
const { data, isLoading, error } = useQuery(
trpc.items.list.queryOptions({ limit: 20 })
);
if (isLoading) return <Loading />;
if (error) return <Error error={error} />;
return <Display data={data} />;
```
## Mutation
```typescript
const mutation = useMutation(trpc.items.create.mutationOptions());
mutation.mutate({ name: "Item" });
```
## Invalidation
```typescript
const queryClient = useQueryClient();
const mutation = useMutation(
trpc.items.update.mutationOptions({
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: trpc.items.list.queryKey(),
});
},
})
);
```
## Infinite Scroll
```typescript
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery(
trpc.items.list.queryOptions(),
{
getNextPageParam: (last) => last.nextCursor,
}
);
```
## Conditional Query
```typescript
const { data } = useQuery(
trpc.user.get.queryOptions({ id }),
{ enabled: !!id }
);
```
```
### references/LINKS.md
```markdown
# Client Links
## httpBatchLink (Batching Requests)
```typescript
import { createTRPCClient, httpBatchLink } from "@trpc/client";
const client = createTRPCClient<AppRouter>({
links: [
httpBatchLink({
url: "/api/trpc",
maxURLLength: 2083, // Optional
}),
],
});
```
Batches multiple requests into single HTTP call. Improves performance.
## httpLink (Single Requests)
```typescript
import { createTRPCClient, httpLink } from "@trpc/client";
const client = createTRPCClient<AppRouter>({
links: [
httpLink({
url: "/api/trpc",
}),
],
});
```
Each request is separate HTTP call.
## Split Links (Route by Operation)
```typescript
import { splitLink, httpLink, httpBatchLink } from "@trpc/client";
const client = createTRPCClient<AppRouter>({
links: [
splitLink({
condition: (op) => op.type === "subscription",
trueLink: wsLink(), // Subscriptions use WebSocket
falseLink: httpBatchLink(), // Queries/mutations use HTTP
}),
],
});
```
## Headers
```typescript
httpBatchLink({
url: "/api/trpc",
headers: () => ({
authorization: `Bearer ${getToken()}`,
"x-custom-header": "value",
}),
});
```
## Transformer (superjson)
```typescript
import superjson from "superjson";
const client = createTRPCClient<AppRouter>({
links: [httpBatchLink({ url: "/api/trpc" })],
transformer: superjson,
});
```
Enables Date, Map, Set, and other complex types serialization.
```
### references/SCHEMAS.md
```markdown
# Complex Zod Schemas
## Nested Objects
```typescript
publicProcedure
.input(z.object({
user: z.object({
id: z.string().uuid(),
name: z.string().min(1).max(100),
email: z.string().email(),
}),
preferences: z.object({
theme: z.enum(["light", "dark"]),
notifications: z.boolean(),
}),
}))
.query(async ({ input }) => { /* ... */ });
```
## Arrays with Validation
```typescript
publicProcedure
.input(z.object({
items: z.array(z.object({
id: z.string(),
quantity: z.number().int().positive(),
})),
}))
.mutation(async ({ input }) => { /* ... */ });
```
## Optional Fields with Defaults
```typescript
publicProcedure
.input(z.object({
search: z.string().optional(),
limit: z.number().min(1).max(100).default(20),
page: z.number().int().positive().default(1),
}))
.query(async ({ input }) => { /* ... */ });
```
## Enums and Unions
```typescript
publicProcedure
.input(z.object({
type: z.enum(["user", "admin", "guest"]),
status: z.union([z.literal("active"), z.literal("inactive")]),
}))
.query(async ({ input }) => { /* ... */ });
```
## Custom Validation
```typescript
publicProcedure
.input(z.object({
password: z.string().min(8).refine(
(val) => /[A-Z]/.test(val),
{ message: "Must contain uppercase letter" }
),
}))
.mutation(async ({ input }) => { /* ... */ });
```
```
### references/CLIENT_ERRORS.md
```markdown
# Client Error Handling
## TRPCError Type
```typescript
import type { TRPCError } from "@trpc/client";
const { error } = useQuery(trpc.items.get.queryOptions({ id }));
if (error?.data?.code === "NOT_FOUND") {
return <div>Item not found</div>;
}
if (error?.data?.code === "UNAUTHORIZED") {
return <div>Please log in</div>;
}
```
## Error Codes
| Code | Meaning |
|------|----------|
| BAD_REQUEST | Invalid input |
| UNAUTHORIZED | Not logged in |
| FORBIDDEN | No permission |
| NOT_FOUND | Resource missing |
| INTERNAL_SERVER_ERROR | Server error |
| TOO_MANY_REQUESTS | Rate limited |
## Global Error Handling (TanStack Query)
```typescript
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (error, query) => {
console.error("Query error:", error);
if (error?.data?.code === "UNAUTHORIZED") {
// Redirect to login
window.location.href = "/login";
}
},
}),
});
```
## Mutation Error Handling
```typescript
const mutation = useMutation(
trpc.items.create.mutationOptions({
onError: (error) => {
if (error?.data?.code === "BAD_REQUEST") {
toast.error("Invalid input");
} else if (error?.data?.code === "UNAUTHORIZED") {
toast.error("Not logged in");
} else {
toast.error("Something went wrong");
}
},
})
);
```
```
### references/INSTALL.md
```markdown
# Install Packages
## Core Backend
```bash
pnpm add @trpc/server zod superjson
npm install @trpc/server zod superjson
yarn add @trpc/server zod superjson
bun add @trpc/server zod superjson
```
## Server Adapter (Choose One)
```bash
# Next.js (built-in, no extra package)
pnpm add @trpc/server
# Express
pnpm add @trpc/server/adapters/express express
# Fastify
pnpm add @trpc/server/adapters/fastify fastify
# Hono
pnpm add @hono/trpc-server hono
# Bun
pnpm add @trpc/server/adapters/bun
```
## Frontend (React)
```bash
pnpm add @trpc/client @trpc/tanstack-react-query @tanstack/react-query
npm install @trpc/client @trpc/tanstack-react-query @tanstack/react-query
yarn add @trpc/client @trpc/tanstack-react-query @tanstack/react-query
bun add @trpc/client @trpc/tanstack-react-query @tanstack/react-query
```
## Notes
- Use your package manager of choice
- Versions are managed by your package.json
- Check [tRPC docs](https://trpc.io) for latest
```