Install
$ agentstack add skill-dallay-agents-skills-trpc-type-safety ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
tRPC — End-to-End Type Safety
Build type-safe APIs where TypeScript types flow automatically from server to client with zero code generation. Define your API once, get automatic type inference everywhere.
When to Use
- Full-stack TypeScript applications (Next.js, T3 stack, monorepos).
- Projects where client and server share a TypeScript codebase.
- Internal APIs where you control both ends — no external consumers.
- Apps using React Query for data fetching and caching.
- Teams wanting REST-like simplicity with GraphQL-like type safety.
Avoid when: public APIs for non-TypeScript clients, polyglot microservices, mobile apps in Swift/Kotlin, or when you need OpenAPI documentation for external developers.
Quick Start
# Server
npm install @trpc/server zod
# React/Next.js client
npm install @trpc/client @trpc/react-query @tanstack/react-query
Server — Define Router
// server/trpc.ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
const t = initTRPC.create();
export const appRouter = t.router({
hello: t.procedure
.input(z.object({ name: z.string() }))
.query(({ input }) => {
return { greeting: `Hello ${input.name}` };
}),
createPost: t.procedure
.input(z.object({ title: z.string(), content: z.string() }))
.mutation(async ({ input }) => {
return { id: 1, ...input };
}),
});
export type AppRouter = typeof appRouter;
Client — React Usage
// client/trpc.ts
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../server/trpc';
export const trpc = createTRPCReact();
// Component — fully typed, zero codegen
function MyComponent() {
const { data } = trpc.hello.useQuery({ name: 'World' });
const createPost = trpc.createPost.useMutation();
return {data?.greeting};
}
Router Definition
Nested Routers (Namespacing)
const userRouter = t.router({
getById: t.procedure
.input(z.string())
.query(({ input }) => getUser(input)),
create: t.procedure
.input(z.object({ name: z.string(), email: z.string() }))
.mutation(({ input }) => createUser(input)),
});
const postRouter = t.router({
list: t.procedure.query(() => getPosts()),
create: t.procedure
.input(z.object({ title: z.string() }))
.mutation(({ input }) => createPost(input)),
});
export const appRouter = t.router({
user: userRouter,
post: postRouter,
});
// Client: trpc.user.getById.useQuery('123')
// Client: trpc.post.list.useQuery()
Recommended File Structure
server/
├── trpc.ts # tRPC instance, context, base procedures
├── routers/
│ ├── user.ts # User procedures
│ ├── post.ts # Post procedures
│ └── index.ts # Merge all routers → appRouter
└── schemas/
├── user.ts # Zod schemas (shared validation)
└── post.ts
Procedures (Query & Mutation)
const router = t.router({
// Query — read data (GET, cached by React Query)
getUser: t.procedure
.input(z.string())
.query(({ input }) => {
return db.user.findUnique({ where: { id: input } });
}),
// Query with complex input
searchUsers: t.procedure
.input(z.object({
query: z.string(),
limit: z.number().default(10),
}))
.query(({ input }) => {
return db.user.findMany({
where: { name: { contains: input.query } },
take: input.limit,
});
}),
// Mutation — write data (POST, not cached)
createUser: t.procedure
.input(z.object({
name: z.string().min(3),
email: z.string().email(),
}))
.mutation(async ({ input }) => {
return await db.user.create({ data: input });
}),
});
Context and Middleware
Creating Context
export async function createContext(opts: CreateNextContextOptions) {
const session = await getSession(opts.req);
return {
session,
db: prisma,
};
}
export type Context = Awaited>;
const t = initTRPC.context().create();
Authentication Middleware
const isAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.session?.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return next({
ctx: {
...ctx,
user: ctx.session.user, // Narrowed type — guaranteed non-null
},
});
});
// Reusable procedure builders
const protectedProcedure = t.procedure.use(isAuthed);
const router = t.router({
// Public
getPublicPosts: t.procedure.query(() => getPosts()),
// Protected — ctx.user is guaranteed
getMyPosts: protectedProcedure.query(({ ctx }) => {
return getPostsByUser(ctx.user.id);
}),
});
Logging Middleware
const loggerMiddleware = t.middleware(async ({ path, type, next }) => {
const start = Date.now();
const result = await next();
console.log(`${type} ${path} — ${Date.now() - start}ms`);
return result;
});
Error Handling
Throwing Typed Errors
import { TRPCError } from '@trpc/server';
const router = t.router({
getUser: t.procedure
.input(z.string())
.query(async ({ input }) => {
const user = await db.user.findUnique({ where: { id: input } });
if (!user) {
throw new TRPCError({
code: 'NOT_FOUND',
message: `User ${input} not found`,
});
}
return user;
}),
});
Error Codes Reference
| Code | HTTP | Use Case | |-------------------------|------|---------------------| | BAD_REQUEST | 400 | Invalid input | | UNAUTHORIZED | 401 | Not authenticated | | FORBIDDEN | 403 | Not authorized | | NOT_FOUND | 404 | Resource missing | | CONFLICT | 409 | Resource conflict | | TOO_MANY_REQUESTS | 429 | Rate limit exceeded | | INTERNAL_SERVER_ERROR | 500 | Server error |
Error Formatter (Zod Integration)
import { ZodError } from 'zod';
const t = initTRPC.context().create({
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError:
error.code === 'BAD_REQUEST' && error.cause instanceof ZodError
? error.cause.flatten()
: null,
},
};
},
});
React Integration
Query with Options
function UserProfile({ userId }: { userId: string }) {
const { data, isLoading, error } = trpc.user.getById.useQuery(userId, {
staleTime: 5 * 60 * 1000,
retry: 3,
});
if (isLoading) return Loading...;
if (error) return Error: {error.message};
return {data.name};
}
Mutation with Cache Invalidation
function CreatePostForm() {
const utils = trpc.useContext();
const createPost = trpc.post.create.useMutation({
onSuccess: () => {
utils.post.list.invalidate(); // Refetch post list
},
});
return (
createPost.mutate({ title: 'New Post' })}
disabled={createPost.isLoading}
>
{createPost.isLoading ? 'Creating...' : 'Create'}
);
}
Optimistic Updates
const createPost = trpc.post.create.useMutation({
onMutate: async (newPost) => {
await utils.post.list.cancel();
const previous = utils.post.list.getData();
utils.post.list.setData(undefined, (old) => [
...(old ?? []),
{ id: 'temp', ...newPost },
]);
return { previous };
},
onError: (_err, _newPost, context) => {
utils.post.list.setData(undefined, context?.previous);
},
onSettled: () => {
utils.post.list.invalidate();
},
});
Next.js Integration
API Route (App Router)
// app/api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from '../../../../server/routers';
import { createContext } from '../../../../server/context';
const handler = (req: Request) =>
fetchRequestHandler({
endpoint: '/api/trpc',
req,
router: appRouter,
createContext,
});
export { handler as GET, handler as POST };
Provider Setup
// app/providers.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import { useState } from 'react';
import { trpc } from './trpc';
export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => new QueryClient());
const [trpcClient] = useState(() =>
trpc.createClient({
links: [httpBatchLink({ url: '/api/trpc' })],
})
);
return (
{children}
);
}
Testing
Unit Testing with Caller
import { createCaller } from '../routers';
describe('User Router', () => {
it('should create user', async () => {
const caller = createCaller({
db: mockDb,
session: null,
});
const result = await caller.user.create({
name: 'Alice',
email: 'alice@example.com',
});
expect(result).toMatchObject({
name: 'Alice',
email: 'alice@example.com',
});
});
});
Mocking Context for Protected Routes
const authedCaller = createCaller({
db: mockDb,
session: {
user: { id: '1', email: 'alice@example.com' },
},
});
it('should get current user', async () => {
const user = await authedCaller.user.getMe();
expect(user.name).toBe('Alice');
});
TypeScript Inference Patterns
import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server';
import type { AppRouter } from './server';
// Extract input/output types from any procedure
type RouterInputs = inferRouterInputs;
type RouterOutputs = inferRouterOutputs;
type CreateUserInput = RouterInputs['user']['create'];
type User = RouterOutputs['user']['getById'];
// Use in components for prop typing
function UserCard({ user }: { user: User }) {
return {user.name};
}
Best Practices
✅ DO
- Export
type AppRouter, never the router implementation, to the client. - Use Zod schemas for all input validation — they give you runtime safety AND TypeScript types.
- Create reusable procedure builders (
protectedProcedure,adminProcedure) via middleware. - Organize routers by domain (user, post, admin) in separate files.
- Use
httpBatchLinkto reduce HTTP requests — multiple queries become one round-trip. - Set
staleTimeon React Query hooks to avoid unnecessary refetches. - Share Zod schemas between client and server for form validation.
- Use
inferRouterInputs/inferRouterOutputsfor component prop types.
❌ DON'T
- Import server code into client bundles — only import
typefrom the server. - Skip input validation — even with TypeScript, runtime validation catches bad data.
- Put heavy computation in context creation — keep it lazy.
- Use tRPC for public APIs consumed by non-TypeScript clients.
- Nest routers more than 2 levels deep — it hurts discoverability.
- Forget cache invalidation after mutations — stale data is a common bug.
- Use
.output()validation on every procedure — it adds runtime overhead. Reserve for critical
data.
- Expose internal error details in production — sanitize with
errorFormatter.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: dallay
- Source: dallay/agents-skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.