T

trpc-type-safety

by @bobmatnycv
4.3(37)

tRPCフレームワークに特化し、エンドツーエンドの型安全な開発ガイダンスを提供し、フロントエンドとバックエンドのAPI呼び出しにおける型の一貫性を確保し、ランタイムエラーを削減します。

trpctypescriptend-to-end-type-safetyapi-developmentrpcGitHub
インストール方法
npx skills add bobmatnyc/claude-mpm-skills --skill trpc-type-safety
compare_arrows

Before / After 効果比較

1
使用前

従来の開発では、フロントエンドとバックエンドのインターフェースの型不一致がランタイムエラーを引き起こしやすく、デバッグが困難で、開発効率とシステム安定性に深刻な影響を与えていました。

使用後

tRPCの型安全性ガイドを活用することで、エンドツーエンドの型の一貫性を確保し、ランタイムエラーを大幅に削減し、開発効率とシステム信頼性を向上させます。

SKILL.md

trpc-type-safety

tRPC - End-to-End Type Safety

progressive_disclosure: entry_point: summary sections:

  • id: summary title: "tRPC Overview" tokens: 70 next: [when_to_use, quick_start]
  • id: when_to_use title: "When to Use tRPC" tokens: 150 next: [quick_start, core_concepts]
  • id: quick_start title: "Quick Start" tokens: 300 next: [core_concepts, router_definition]
  • id: core_concepts title: "Core Concepts" tokens: 400 next: [router_definition, procedures]
  • id: router_definition title: "Router Definition" tokens: 350 next: [procedures, context]
  • id: procedures title: "Procedures (Query & Mutation)" tokens: 400 next: [input_validation, context]
  • id: input_validation title: "Input Validation with Zod" tokens: 350 next: [context, middleware]
  • id: context title: "Context Management" tokens: 400 next: [middleware, error_handling]
  • id: middleware title: "Middleware" tokens: 400 next: [error_handling, client_setup]
  • id: error_handling title: "Error Handling" tokens: 350 next: [client_setup, react_integration]
  • id: client_setup title: "Client Setup" tokens: 400 next: [react_integration, nextjs_integration]
  • id: react_integration title: "React Query Integration" tokens: 450 next: [nextjs_integration, subscriptions]
  • id: nextjs_integration title: "Next.js App Router Integration" tokens: 500 next: [subscriptions, file_uploads]
  • id: subscriptions title: "Real-time Subscriptions" tokens: 400 next: [file_uploads, batching]
  • id: file_uploads title: "File Uploads" tokens: 300 next: [batching, typescript_inference]
  • id: batching title: "Batch Requests & Data Loaders" tokens: 350 next: [typescript_inference, testing]
  • id: typescript_inference title: "TypeScript Inference Patterns" tokens: 300 next: [testing, production_patterns]
  • id: testing title: "Testing Strategies" tokens: 400 next: [production_patterns, comparison]
  • id: production_patterns title: "Production Patterns" tokens: 450 next: [comparison, migration]
  • id: comparison title: "Comparison with REST & GraphQL" tokens: 250 next: [migration, best_practices]
  • id: migration title: "Migration from REST" tokens: 300 next: [best_practices]
  • id: best_practices title: "Best Practices & Performance" tokens: 400

Summary

tRPC enables end-to-end type safety between TypeScript clients and servers without code generation. Define your API once, get automatic type inference everywhere.

Key Benefits: Zero codegen, TypeScript inference, React Query integration, minimal boilerplate.

When to Use tRPC

✅ Perfect For:

  • Full-stack TypeScript applications (Next.js, T3 stack)

  • Projects where client and server share TypeScript codebase

  • Teams wanting REST-like simplicity with GraphQL-like type safety

  • Apps using React Query for data fetching

  • Internal APIs where you control both client and server

❌ Avoid When:

  • Public APIs consumed by non-TypeScript clients

  • Microservices in different languages

  • Mobile apps using Swift/Kotlin (use REST/GraphQL instead)

  • Need API documentation for external developers (OpenAPI better)

When to Choose:

  • tRPC: Full-stack TypeScript, monorepo, internal tools

  • REST: Public APIs, language-agnostic, broad compatibility

  • GraphQL: Complex data graphs, multiple clients, flexible queries

Quick Start

Installation

# Server dependencies
npm install @trpc/server zod

# React/Next.js client dependencies
npm install @trpc/client @trpc/react-query @tanstack/react-query

Define Router (Server)

// 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 }) => {
      // Save to database
      return { id: 1, ...input };
    }),
});

export type AppRouter = typeof appRouter;

Use in Client (React)

// client/trpc.ts
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../server/trpc';

export const trpc = createTRPCReact<AppRouter>();

// Component
function MyComponent() {
  const { data } = trpc.hello.useQuery({ name: 'World' });
  const createPost = trpc.createPost.useMutation();

  return <div>{data?.greeting}</div>; // Fully typed!
}

Next: Learn core concepts or dive into router definition.

Core Concepts

The tRPC Philosophy

tRPC provides type-safe remote procedure calls by sharing TypeScript types between client and server. No code generation—just TypeScript's inference.

Key Components

  • Router: Collection of procedures (API endpoints)

  • Procedure: Single API operation (query or mutation)

  • Context: Request-scoped data (user, database, etc.)

  • Middleware: Intercept/modify requests (auth, logging)

  • Input/Output: Validated with Zod schemas

Type Flow

// Server defines types
const router = t.router({
  getUser: t.procedure
    .input(z.string())
    .query(({ input }) => ({ id: input, name: 'Alice' })),
});

// Client gets automatic types
const user = await trpc.getUser.query('123');
// user is typed as { id: string, name: string }

Architecture Pattern

┌─────────────┐     Type-safe     ┌──────────────┐
│   Client    │ ←────────────────→ │    Server    │
│ (React)     │   No codegen!      │   (Node.js)  │
└─────────────┘                    └──────────────┘
      ↓                                    ↓
 React Query                          tRPC Router
 (caching)                            (procedures)

Advantages:

  • Changes propagate instantly (no build step)

  • Rename refactoring works across client/server

  • Impossible to call wrong types

  • Auto-complete for all API methods

Router Definition

Basic Router Structure

import { initTRPC } from '@trpc/server';

const t = initTRPC.create();

export const appRouter = t.router({
  // Procedures go here
});

export type AppRouter = typeof appRouter;

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 usage:
// trpc.user.getById.useQuery('123')
// trpc.post.list.useQuery()

Router Merging

import { adminRouter } from './admin';
import { publicRouter } from './public';

export const appRouter = t.mergeRouters(publicRouter, adminRouter);

Router Organization Best Practices

server/
├── trpc.ts           # tRPC instance, context, middleware
├── routers/
│   ├── user.ts       # User-related procedures
│   ├── post.ts       # Post-related procedures
│   └── index.ts      # Combine all routers
└── index.ts          # Export AppRouter type

Procedures (Query & Mutation)

Query Procedures (Read Operations)

const router = t.router({
  // Simple query
  getUser: t.procedure
    .input(z.string())
    .query(({ input }) => {
      return db.user.findUnique({ where: { id: input } });
    }),

  // Query with multiple inputs
  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 Procedures (Write Operations)

const router = t.router({
  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 });
    }),

  updateUser: t.procedure
    .input(z.object({
      id: z.string(),
      data: z.object({
        name: z.string().optional(),
        email: z.string().email().optional(),
      }),
    }))
    .mutation(async ({ input }) => {
      return await db.user.update({
        where: { id: input.id },
        data: input.data,
      });
    }),
});

Query vs Mutation

Aspect Query Mutation

Purpose Read data Modify data

HTTP Method GET POST

Caching Cached by React Query Not cached

Idempotent Yes No

Side Effects None Database writes, emails, etc.

Output Typing

const router = t.router({
  getUser: t.procedure
    .input(z.string())
    .output(z.object({ id: z.string(), name: z.string() })) // Optional
    .query(({ input }) => {
      return { id: input, name: 'Alice' };
    }),
});

Note: Output validation adds runtime overhead—use for critical data only.

Input Validation with Zod

Why Zod?

tRPC uses Zod for runtime type validation and TypeScript inference. Zod schemas provide:

  • Runtime validation (prevent invalid data)

  • TypeScript types (auto-inferred from schema)

  • Transformation (parse, coerce, default values)

Basic Validation

import { z } from 'zod';

const router = t.router({
  createPost: t.procedure
    .input(z.object({
      title: z.string().min(5).max(100),
      content: z.string(),
      published: z.boolean().default(false),
      tags: z.array(z.string()).optional(),
    }))
    .mutation(({ input }) => {
      // input is fully typed and validated
      return createPost(input);
    }),
});

Advanced Validation

const createUserInput = z.object({
  email: z.string().email(),
  password: z.string().min(8),
  age: z.number().int().min(18),
  role: z.enum(['user', 'admin']),
  metadata: z.record(z.string(), z.unknown()).optional(),
});

const router = t.router({
  createUser: t.procedure
    .input(createUserInput)
    .mutation(({ input }) => {
      // All validation passed
      return saveUser(input);
    }),
});

Transformations

const router = t.router({
  getUser: t.procedure
    .input(
      z.object({
        id: z.string().transform((id) => parseInt(id, 10)),
      })
    )
    .query(({ input }) => {
      // input.id is now a number
      return db.user.findUnique({ where: { id: input.id } });
    }),
});

Reusable Schemas

// schemas/user.ts
export const CreateUserSchema = z.object({
  name: z.string(),
  email: z.string().email(),
});

export const UpdateUserSchema = CreateUserSchema.partial().extend({
  id: z.string(),
});

// routers/user.ts
const router = t.router({
  create: t.procedure.input(CreateUserSchema).mutation(/*...*/),
  update: t.procedure.input(UpdateUserSchema).mutation(/*...*/),
});

Context Management

What is Context?

Context provides request-scoped data to all procedures—authentication, database connections, logging, etc.

Creating Context

import { inferAsyncReturnType } from '@trpc/server';
import { CreateNextContextOptions } from '@trpc/server/adapters/next';

export async function createContext(opts: CreateNextContextOptions) {
  const session = await getSession(opts.req);

  return {
    session,
    db: prisma,
    req: opts.req,
    res: opts.res,
  };
}

export type Context = inferAsyncReturnType<typeof createContext>;

const t = initTRPC.context<Context>().create();

Using Context in Procedures

const router = t.router({
  getMe: t.procedure.query(({ ctx }) => {
    if (!ctx.session?.user) {
      throw new TRPCError({ code: 'UNAUTHORIZED' });
    }

    return ctx.db.user.findUnique({
      where: { id: ctx.session.user.id },
    });
  }),

  createPost: t.procedure
    .input(z.object({ title: z.string() }))
    .mutation(async ({ ctx, input }) => {
      return ctx.db.post.create({
        data: {
          title: input.title,
          authorId: ctx.session.user.id,
        },
      });
    }),
});

Context Best Practices

// ✅ Good: Lazy database connection
export async function createContext(opts: CreateNextContextOptions) {
  return {
    getDB: () => prisma, // Lazy
    session: await getSession(opts.req),
  };
}

// ❌ Bad: Heavy computation in context
export async function createContext(opts: CreateNextContextOptions) {
  const allUsers = await prisma.user.findMany(); // Too expensive!
  return { allUsers };
}

Middleware

What is Middleware?

Middleware intercepts procedure calls to add cross-cutting concerns: logging, timing, authentication, rate limiting.

Basic Middleware

const loggerMiddleware = t.middleware(async ({ path, type, next }) => {
  const start = Date.now();
  console.log(`→ ${type} ${path}`);

  const result = await next();

  const duration = Date.now() - start;
  console.log(`✓ ${type} ${path} - ${duration}ms`);

  return result;
});

const loggedProcedure = t.procedure.use(loggerMiddleware);

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, // Narrow type
    },
  });
});

// Protected procedure builder
const protectedProcedure = t.procedure.use(isAuthed);

const router = t.router({
  // Public
  getPublicPosts: t.procedure.query(() => getPosts()),

  // Protected - requires authentication
  getMyPosts: protectedProcedure.query(({ ctx }) => {
    // ctx.user is guaranteed to exist
    return getPostsByUser(ctx.user.id);
  }),
});

Chaining Middleware

const timingMiddleware = t.middleware(async ({ next }) => {
  const start = performance.now();
  const result = await next();
  console.log(`Execution time: ${performance.now() - start}ms`);
  return result;
});

const rateLimitMiddleware = t.middleware(async ({ ctx, next }) => {
  await checkRateLimit(ctx.session?.user?.id);
  return next();
});

const protectedProcedure = t.procedure
  .use(timingMiddleware)
  .use(rateLimitMiddleware)
  .use(isAuthed);

Context Transformation

const enrichContextMiddleware = t.middleware(async ({ ctx, next }) => {
  const user = ctx.session?.user
    ? await ctx.db.user.findUnique({ where: { id: ctx.session.user.id } })
    : null;

  return next({
    ctx: {
      ...ctx,
      user, // Full user object
    },
  });
});

Error Handling

TRPCError

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`,
        });
 

...

ユーザーレビュー (0)

レビューを書く

効果
使いやすさ
ドキュメント
互換性

レビューなし

統計データ

インストール数1.1K
評価4.3 / 5.0
バージョン
更新日2026年5月20日
比較事例1 件

ユーザー評価

4.3(37)
5
59%
4
41%
3
0%
2
0%
1
0%

この Skill を評価

0.0

対応プラットフォーム

🔧Claude Code
🔧OpenClaw
🔧OpenCode
🔧Codex
🔧Gemini CLI
🔧GitHub Copilot
🔧Amp
🔧Kimi CLI

タイムライン

作成2026年3月17日
最終更新2026年5月20日