AgentStack
SKILL verified MIT Self-run

Trpc Type Safety

skill-dallay-agents-skills-trpc-type-safety · by dallay

>-

No reviews yet
0 installs
9 views
0.0% view→install

Install

$ agentstack add skill-dallay-agents-skills-trpc-type-safety

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No 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.

Are you the author of Trpc Type Safety? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

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 httpBatchLink to reduce HTTP requests — multiple queries become one round-trip.
  • Set staleTime on React Query hooks to avoid unnecessary refetches.
  • Share Zod schemas between client and server for form validation.
  • Use inferRouterInputs/inferRouterOutputs for component prop types.

❌ DON'T

  • Import server code into client bundles — only import type from 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.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.