Gojiberry AI
AI agents that find and contact high-intent leads for you
Try Gojiberry free →
Hermes Agent
Run your Hermes agent, fully managed
Launch on Hostinger →
Hostinger VPS
Spin up a VPS in one click, 20% off
Launch on Hostinger →
Firecrawl
Crawl and scrape any site into clean data
Try Firecrawl free →
CodeRabbit
AI code reviews for every PR
Try CodeRabbit free →
Context.dev
One API to scrape, enrich, and extract the web
Start building free →
Jotform
Forms, workflows, and AI Agents for your team
Try Jotform free →
Gojiberry AI
AI agents that find and contact high-intent leads for you
Try Gojiberry free →
OpenClaw
Deploy a managed OpenClaw agent in 60 seconds
Launch on Hostinger →
Sponsor here
9/10 sponsor slots taken — 1 left
Claim it →
Claude Market
Menu
SkillsMCPPluginsMarketplacesNewsletterSubmit MCPSkillPluginMCPMCP, plugin, or skillAdvertise
Claude Market
SkillsMCPPluginsMarketplacesNewsletterSubmit MCPSkillPluginMCPMCP, plugin, or skillAdvertise
Skills/bobmatnyc/claude-mpm-skills/trpc-type-safety
trpc-type-safety logo

trpc-type-safety

bobmatnyc/claude-mpm-skills
1K installs53 stars
Run it on Hostinger, 20% off →Your friend gets 20% off too, using this linkFree API →|No Code|View on GitHub|Create your own skill →

Installation

npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill trpc-type-safety

Summary

tRPC end-to-end type-safe APIs for TypeScript with React Query integration and full-stack type safety

SKILL.md

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

  1. Router: Collection of procedures (API endpoints)
  2. Procedure: Single API operation (query or mutation)
  3. Context: Request-scoped data (user, database, etc.)
  4. Middleware: Intercept/modify requests (auth, logging)
  5. 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

AspectQueryMutation
PurposeRead dataModify data
HTTP MethodGETPOST
CachingCached by React QueryNot cached
IdempotentYesNo
Side EffectsNoneDatabase 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`,
        });
      }

      return user;
    }),
});

Error Codes

CodeHTTP StatusUse Case
BAD_REQUEST400Invalid input
UNAUTHORIZED401Not authenticated
FORBIDDEN403Not authorized
NOT_FOUND404Resource not found
TIMEOUT408Request timeout
CONFLICT409Resource conflict
PRECONDITION_FAILED412Precondition failed
PAYLOAD_TOO_LARGE413Request too large
TOO_MANY_REQUESTS429Rate limit exceeded
CLIENT_CLOSED_REQUEST499Client closed connection
INTERNAL_SERVER_ERROR500Server error

Custom Error Handling

const router = t.router({
  deleteUser: t.procedure
    .input(z.string())
    .mutation(async ({ input, ctx }) => {
      try {
        return await ctx.db.user.delete({ where: { id: input } });
      } catch (error) {
        if (error.code === 'P2025') { // Prisma not found
          throw new TRPCError({
            code: 'NOT_FOUND',
            message: 'User not found',
            cause: error,
          });
        }
        throw new TRPCError({
          code: 'INTERNAL_SERVER_ERROR',
          message: 'Failed to delete user',
          cause: error,
        });
      }
    }),
});

Error Formatting

const t = initTRPC.context<Context>().create({
  errorFormatter({ shape, error }) {
    return {
      ...shape,
      data: {
        ...shape.data,
        zodError:
          error.code === 'BAD_REQUEST' && error.cause instanceof ZodError
            ? error.cause.flatten()
            : null,
      },
    };
  },
});

Client-Side Error Handling

function MyComponent() {
  const mutation = trpc.createUser.useMutation({
    onError: (error) => {
      if (error.data?.code === 'UNAUTHORIZED') {
        router.push('/login');
      } else {
        toast.error(error.message);
      }
    },
  });
}

---

Client Setup

Vanilla Client

import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from './server';

const client = createTRPCProxyClient<AppRouter>({
  links: [
    httpBatchLink({
      url: 'http://localhost:3000/api/trpc',
    }),
  ],
});

// Usage
const user = await client.user.getById.query('123');
const newPost = await client.post.create.mutate({ title: 'Hello' });

React Client Setup

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

export const trpc = createTRPCReact<AppRouter>();

// _app.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import { useState } from 'react';
import { trpc } from '../utils/trpc';

export default function App({ Component, pageProps }: AppProps) {
  const [queryClient] = useState(() => new QueryClient());
  const [trpcClient] = useState(() =>
    trpc.createClient({
      links: [
        httpBatchLink({
          url: 'http://localhost:3000/api/trpc',
        }),
      ],
    })
  );

  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>
        <Component {...pageProps} />
      </QueryClientProvider>
    </trpc.Provider>
  );
}

Next.js API Route

// pages/api/trpc/[trpc].ts
import { createNextApiHandler } from '@trpc/server/adapters/next';
import { appRouter } from '../../../server/routers';
import { createContext } from '../../../server/context';

export default createNextApiHandler({
  router: appRouter,
  createContext,
});

Headers & Authentication

const trpcClient = trpc.createClient({
  links: [
    httpBatchLink({
      url: 'http://localhost:3000/api/trpc',
      headers: async () => {
        const token = await getAuthToken();
        return {
          authorization: token ? `Bearer ${token}` : undefined,
        };
      },
    }),
  ],
});

---

React Query Integration

useQuery Hook

function UserProfile({ userId }: { userId: string }) {
  const { data, isLoading, error } = trpc.user.getById.useQuery(userId);

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return <div>{data.name}</div>;
}

Query Options

const { data } = trpc.posts.list.useQuery(undefined, {
  refetchOnWindowFocus: false,
  staleTime: 5 * 60 * 1000, // 5 minutes
  cacheTime: 10 * 60 * 1000, // 10 minutes
  retry: 3,
  onSuccess: (data) => console.log('Fetched', data.length, 'posts'),
});

useMutation Hook

function CreatePostForm() {
  const utils = trpc.useContext();

  const createPost = trpc.post.create.useMutation({
    onSuccess: () => {
      // Invalidate and refetch
      utils.post.list.invalidate();
    },
  });

  const handleSubmit = (data: { title: string }) => {
    createPost.mutate(data);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="title" />
      <button disabled={createPost.isLoading}>
        {createPost.isLoading ? 'Creating...' : 'Create'}
      </button>
      {createPost.error && <p>{createPost.error.message}</p>}
    </form>
  );
}

Optimistic Updates

const createPost = trpc.post.create.useMutation({
  onMutate: async (newPost) => {
    // Cancel outgoing refetches
    await utils.post.list.cancel();

    // Snapshot previous value
    const previousPosts = utils.post.list.getData();

    // Optimistically update
    utils.post.list.setData(undefined, (old) => [
      ...(old ?? []),
      { id: 'temp', ...newPost },
    ]);

    return { previousPosts };
  },
  onError: (err, newPost, context) => {
    // Rollback on error
    utils.post.list.setData(undefined, context?.previousPosts);
  },
  onSettled: () => {
    // Refetch after success or error
    utils.post.list.invalidate();
  },
});

Infinite Queries

// Server
const router = t.router({
  posts: t.procedure
    .input(z.object({
      cursor: z.number().optional(),
      limit: z.number().default(10),
    }))
    .query(({ input }) => {
      const posts = getPosts(input.cursor, input.limit);
      return {
        posts,
        nextCursor: posts.length === input.limit ? input.cursor + input.limit : undefined,
      };
    }),
});

// Client
function PostList() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = trpc.posts.useInfiniteQuery(
    { limit: 10 },
    {
      getNextPageParam: (lastPage) => lastPage.nextCursor,
    }
  );

  return (
    <div>
      {data?.pages.map((page) =>
        page.posts.map((post) => <PostCard key={post.id} post={post} />)
      )}
      {hasNextPage && (
        <button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
          Load More
        </button>
      )}
    </div>
  );
}

---

Next.js App Router Integration

Server Components

// app/users/page.tsx (Server Component)
import { createCaller } from '../server/routers';
import { createContext } from '../server/context';

export default async function UsersPage() {
  const ctx = await createContext({ req: null, res: null });
  const caller = createCaller(ctx);

  const users = await caller.user.list();

  return (
    <div>
      {users.map((user) => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  );
}

Server Actions

// app/actions.ts
'use server';

import { createCaller } from '../server/routers';
import { createContext } from '../server/context';

export async function createPost(formData: FormData) {
  const ctx = await createContext({ req: null, res: null });
  const caller = createCaller(ctx);

  return caller.post.create({
    title: formData.get('title') as string,
    content: formData.get('content') as string,
  });
}

App Router Provider

// 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 (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>
        {children}
      </QueryClientProvider>
    </trpc.Provider>
  );
}

// app/layout.tsx
import { Providers } from './providers';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

Client Components in App Router

// app/posts/create-button.tsx
'use client';

import { trpc } from '../trpc';

export function CreatePostButton() {
  const createPost = trpc.post.create.useMutation();

  return (
    <button onClick={() => createPost.mutate({ title: 'New Post' })}>
      Create Post
    </button>
  );
}

API Route Handler (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 };

---

Real-time Subscriptions

WebSocket Setup (Server)

import { applyWSSHandler } from '@trpc/server/adapters/ws';
import ws from 'ws';

const wss = new ws.Server({ port: 3001 });

applyWSSHandler({
  wss,
  router: appRouter,
  createContext,
});

console.log('WebSocket server listening on port 3001');

Subscription Procedure

import { observable } from '@trpc/server/observable';
import { EventEmitter } from 'events';

const ee = new EventEmitter();

const router = t.router({
  onPostAdd: t.procedure.subscription(() => {
    return observable<Post>((emit) => {
      const onAdd = (data: Post) => emit.next(data);

      ee.on('add', onAdd);

      return () => {
        ee.off('add', onAdd);
      };
    });
  }),

  createPost: t.procedure
    .input(z.object({ title: z.string() }))
    .mutation(({ input }) => {
      const post = { id: Date.now().toString(), ...input };
      ee.emit('add', post); // Emit to subscribers
      return post;
    }),
});

Client WebSocket Setup

import { createWSClient, wsLink } from '@trpc/client';

const wsClient = createWSClient({
  url: 'ws://localhost:3001',
});

const trpcClient = trpc.createClient({
  links: [
    wsLink({
      client: wsClient,
    }),
  ],
});

useSubscription Hook

function PostFeed() {
  const [posts, setPosts] = useState<Post[]>([]);

  trpc.onPostAdd.useSubscription(undefined, {
    onData: (post) => {
      setPosts((prev) => [post, ...prev]);
    },
    onError: (err) => {
      console.error('Subscription error:', err);
    },
  });

  return (
    <div>
      {posts.map((post) => (
        <PostCard key={post.id} post={post} />
      ))}
    </div>
  );
}

Subscription with Input

// Server
const router = t.router({
  onUserStatusChange: t.procedure
    .input(z.string())
    .subscription(({ input }) => {
      return observable<UserStatus>((emit) => {
        const onChange = (userId: string, status: UserStatus) => {
          if (userId === input) {
            emit.next(status);
          }
        };

        ee.on('statusChange', onChange);
        return () => ee.off('statusChange', onChange);
      });
    }),
});

// Client
trpc.onUserStatusChange.useSubscription('user-123', {
  onData: (status) => console.log('Status:', status),
});

---

File Uploads

Multipart Form Data (Server)

// Next.js API route with file upload
import { NextApiRequest, NextApiResponse } from 'next';
import formidable from 'formidable';
import fs from 'fs';

export const config = {
  api: { bodyParser: false },
};

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const form = formidable({ multiples: false });

  form.parse(req, async (err, fields, files) => {
    if (err) return res.status(500).json({ error: 'Upload failed' });

    const file = files.file as formidable.File;
    const buffer = fs.readFileSync(file.filepath);

    // Upload to S3, etc.
    const url = await uploadToS3(buffer, file.originalFilename);

    res.json({ url });
  });
}

Base64 Upload (tRPC)

// For small files only (<1MB)
const router = t.router({
  uploadAvatar: t.procedure
    .input(z.object({
      fileName: z.string(),
      fileData: z.string(), // Base64
    }))
    .mutation(async ({ input }) => {
      const buffer = Buffer.from(input.fileData, 'base64');
      const url = await uploadToS3(buffer, input.fileName);
      return { url };
    }),
});

// Client
const uploadAvatar = trpc.uploadAvatar.useMutation();

const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  const file = e.target.files?.[0];
  if (!file) return;

  const reader = new FileReader();
  reader.onload = () => {
    const base64 = reader.result as string;
    uploadAvatar.mutate({
      fileName: file.name,
      fileData: base64.split(',')[1], // Remove data:image/...;base64,
    });
  };
  reader.readAsDataURL(file);
};

Signed URL Pattern (Recommended)

// Step 1: Get signed upload URL from tRPC
const router = t.router({
  getUploadUrl: t.procedure
    .input(z.object({
      fileName: z.string(),
      fileType: z.string(),
    }))
    .mutation(async ({ input }) => {
      const signedUrl = await s3.getSignedUrl('putObject', {
        Bucket: 'my-bucket',
        Key: input.fileName,
        ContentType: input.fileType,
        Expires: 60, // 1 minute
      });

      return { uploadUrl: signedUrl, fileUrl: `https://cdn.example.com/${input.fileName}` };
    }),
});

// Step 2: Client uploads directly to S3
async function uploadFile(file: File) {
  // Get signed URL
  const { uploadUrl, fileUrl } = await trpc.getUploadUrl.mutate({
    fileName: file.name,
    fileType: file.type,
  });

  // Upload directly to S3
  await fetch(uploadUrl, {
    method: 'PUT',
    body: file,
    headers: { 'Content-Type': file.type },
  });

  // Save file URL to database via tRPC
  await trpc.user.updateAvatar.mutate({ url: fileUrl });
}

---

Batch Requests & Data Loaders

Automatic Batching

// Client configuration
const trpcClient = trpc.createClient({
  links: [
    httpBatchLink({
      url: 'http://localhost:3000/api/trpc',
      maxBatchSize: 10, // Batch up to 10 requests
    }),
  ],
});

// Multiple calls made close together are batched into one HTTP request
const user1 = trpc.user.getById.useQuery('1');
const user2 = trpc.user.getById.useQuery('2');
const user3 = trpc.user.getById.useQuery('3');
// → Single HTTP request with 3 procedure calls

DataLoader Pattern

import DataLoader from 'dataloader';

// Create DataLoader in context
export async function createContext() {
  const userLoader = new DataLoader(async (ids: readonly string[]) => {
    const users = await db.user.findMany({
      where: { id: { in: [...ids] } },
    });

    // Return in same order as input
    return ids.map((id) => users.find((u) => u.id === id));
  });

  return { userLoader };
}

// Use in procedures
const router = t.router({
  getUser: t.procedure
    .input(z.string())
    .query(({ ctx, input }) => {
      return ctx.userLoader.load(input); // Batched!
    }),

  getPosts: t.procedure.query(async ({ ctx }) => {
    const posts = await db.post.findMany({ take: 10 });

    // N+1 problem solved—all authors fetched in one query
    const postsWithAuthors = await Promise.all(
      posts.map(async (post) => ({
        ...post,
        author: await ctx.userLoader.load(post.authorId),
      }))
    );

    return postsWithAuthors;
  }),
});

Conditional Batching

import { httpBatchLink, httpLink, splitLink } from '@trpc/client';

const trpcClient = trpc.createClient({
  links: [
    splitLink({
      // Batch queries, don't batch mutations
      condition: (op) => op.type === 'query',
      true: httpBatchLink({ url: '/api/trpc' }),
      false: httpLink({ url: '/api/trpc' }),
    }),
  ],
});

---

TypeScript Inference Patterns

Inferring Types from Router

import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server';
import type { AppRouter } from './server';

// Input types
type RouterInputs = inferRouterInputs<AppRouter>;
type CreateUserInput = RouterInputs['user']['create'];

// Output types
type RouterOutputs = inferRouterOutputs<AppRouter>;
type User = RouterOutputs['user']['getById'];

// Use in components
function UserCard({ user }: { user: User }) {
  return <div>{user.name}</div>;
}

Procedure Helpers

import type { inferProcedureInput, inferProcedureOutput } from '@trpc/server';

type CreatePostInput = inferProcedureInput<AppRouter['post']['create']>;
type Post = inferProcedureOutput<AppRouter['post']['getById']>;

Context Type Inference

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

export async function createContext() {
  return {
    db: prisma,
    user: null as User | null,
  };
}

export type Context = inferAsyncReturnType<typeof createContext>;

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

Generic Procedures

// Reusable pagination
function createPaginatedProcedure<T>(
  getData: (cursor: number, limit: number) => Promise<T[]>
) {
  return t.procedure
    .input(z.object({
      cursor: z.number().optional(),
      limit: z.number().default(10),
    }))
    .query(async ({ input }) => {
      const items = await getData(input.cursor ?? 0, input.limit);
      return {
        items,
        nextCursor: items.length === input.limit
          ? (input.cursor ?? 0) + input.limit
          : undefined,
      };
    });
}

const router = t.router({
  posts: createPaginatedProcedure((cursor, limit) =>
    db.post.findMany({ skip: cursor, take: limit })
  ),
  users: createPaginatedProcedure((cursor, limit) =>
    db.user.findMany({ skip: cursor, take: limit })
  ),
});

---

Testing Strategies

Unit Testing Procedures

import { createCaller } from '../routers';

describe('User Router', () => {
  it('should create user', async () => {
    const ctx = {
      db: mockDb,
      session: null,
    };

    const caller = createCaller(ctx);

    const result = await caller.user.create({
      name: 'Alice',
      email: 'alice@example.com',
    });

    expect(result).toMatchObject({
      name: 'Alice',
      email: 'alice@example.com',
    });
  });
});

Integration Testing

import { httpBatchLink } from '@trpc/client';
import { createTRPCProxyClient } from '@trpc/client';
import type { AppRouter } from '../server';

describe('tRPC Integration', () => {
  const client = createTRPCProxyClient<AppRouter>({
    links: [
      httpBatchLink({
        url: 'http://localhost:3000/api/trpc',
      }),
    ],
  });

  it('should fetch user', async () => {
    const user = await client.user.getById.query('123');
    expect(user.id).toBe('123');
  });
});

Mocking Context

import { createCaller } from '../routers';

const mockContext = {
  db: {
    user: {
      findUnique: vi.fn().mockResolvedValue({ id: '1', name: 'Alice' }),
      create: vi.fn(),
    },
  },
  session: {
    user: { id: '1', email: 'alice@example.com' },
  },
};

it('should get current user', async () => {
  const caller = createCaller(mockContext);
  const user = await caller.user.getMe();

  expect(mockContext.db.user.findUnique).toHaveBeenCalledWith({
    where: { id: '1' },
  });
  expect(user.name).toBe('Alice');
});

Testing React Hooks

import { renderHook, waitFor } from '@testing-library/react';
import { createWrapper } from './test-utils';

it('should fetch posts', async () => {
  const { result } = renderHook(() => trpc.post.list.useQuery(), {
    wrapper: createWrapper(),
  });

  await waitFor(() => expect(result.current.isSuccess).toBe(true));

  expect(result.current.data).toHaveLength(10);
});

// test-utils.ts
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import { trpc } from '../utils/trpc';

export function createWrapper() {
  const queryClient = new QueryClient();
  const trpcClient = trpc.createClient({
    links: [httpBatchLink({ url: 'http://localhost:3000/api/trpc' })],
  });

  return ({ children }) => (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>
        {children}
      </QueryClientProvider>
    </trpc.Provider>
  );
}

---

Production Patterns

Error Monitoring

import * as Sentry from '@sentry/node';

const t = initTRPC.context<Context>().create({
  errorFormatter({ shape, error }) {
    // Log to Sentry
    if (error.code === 'INTERNAL_SERVER_ERROR') {
      Sentry.captureException(error);
    }

    return {
      ...shape,
      data: {
        ...shape.data,
        // Don't expose internal errors in production
        message: process.env.NODE_ENV === 'production' && error.code === 'INTERNAL_SERVER_ERROR'
          ? 'Internal server error'
          : shape.message,
      },
    };
  },
});

Rate Limiting

import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, '10 s'),
});

const rateLimitMiddleware = t.middleware(async ({ ctx, next }) => {
  const identifier = ctx.session?.user?.id ?? ctx.req.ip;
  const { success } = await ratelimit.limit(identifier);

  if (!success) {
    throw new TRPCError({
      code: 'TOO_MANY_REQUESTS',
      message: 'Rate limit exceeded',
    });
  }

  return next();
});

Caching

import { Redis } from 'ioredis';

const redis = new Redis(process.env.REDIS_URL);

const router = t.router({
  getUser: t.procedure
    .input(z.string())
    .query(async ({ input }) => {
      // Check cache
      const cached = await redis.get(`user:${input}`);
      if (cached) return JSON.parse(cached);

      // Fetch from database
      const user = await db.user.findUnique({ where: { id: input } });

      // Cache for 5 minutes
      await redis.setex(`user:${input}`, 300, JSON.stringify(user));

      return user;
    }),
});

Request Logging

const loggingMiddleware = t.middleware(async ({ path, type, next, input }) => {
  const start = Date.now();

  console.log(`→ ${type} ${path}`, { input });

  try {
    const result = await next();
    const duration = Date.now() - start;
    console.log(`✓ ${type} ${path} - ${duration}ms`);
    return result;
  } catch (error) {
    const duration = Date.now() - start;
    console.error(`✗ ${type} ${path} - ${duration}ms`, { error });
    throw error;
  }
});

OpenTelemetry Integration

import { trace } from '@opentelemetry/api';

const tracingMiddleware = t.middleware(async ({ path, type, next }) => {
  const tracer = trace.getTracer('trpc');

  return tracer.startActiveSpan(`trpc.${type}.${path}`, async (span) => {
    try {
      const result = await next();
      span.setStatus({ code: 0 }); // OK
      return result;
    } catch (error) {
      span.setStatus({ code: 2, message: error.message }); // ERROR
      span.recordException(error);
      throw error;
    } finally {
      span.end();
    }
  });
});

---

Comparison with REST & GraphQL

Feature Comparison

FeaturetRPCRESTGraphQL
Type SafetyFull (TypeScript)Manual/codegenManual/codegen
Code GenerationNoneOptional (OpenAPI)Required
Learning CurveLowLowMedium/High
Client LibrariesTypeScript onlyAny languageAny language
API DocumentationTypeScript typesOpenAPI/SwaggerSchema/introspection
Public APIs❌ No✅ Yes✅ Yes
Flexible Queries❌ Fixed❌ Fixed✅ Yes
OverfetchingMinimalCommonNone
CachingReact QueryHTTP cachingComplex
Real-timeWebSocketSSE/WebSocketSubscriptions
File UploadsWorkaroundsNativeComplex

When to Choose Each

tRPC:

  • ✅ Full-stack TypeScript monorepo
  • ✅ Internal tools and dashboards
  • ✅ Next.js applications
  • ✅ Rapid development with small teams
  • ❌ Public APIs for external consumers
  • ❌ Multi-language clients

REST:

  • ✅ Public APIs with broad compatibility
  • ✅ Multi-language services
  • ✅ HTTP caching requirements
  • ✅ File uploads and downloads
  • ❌ Complex nested data structures
  • ❌ Need for type safety without codegen

GraphQL:

  • ✅ Complex data graphs
  • ✅ Multiple client types (web, mobile, etc.)
  • ✅ Need for flexible queries
  • ✅ Avoiding overfetching
  • ❌ Simple CRUD operations
  • ❌ Small teams (complexity overhead)

Migration Path

tRPC can coexist with REST/GraphQL:

// Use tRPC for internal, REST for public
const router = t.router({
  internal: internalRouter, // tRPC only
});

// Expose REST endpoints separately
app.get('/api/public/users', publicRestHandler);

---

Migration from REST

Gradual Migration Strategy

  1. Add tRPC alongside REST: Don't rewrite everything at once
  2. New features in tRPC: Start with new endpoints
  3. Migrate high-value endpoints: Focus on complex or frequently used APIs
  4. Keep public APIs in REST: Only migrate internal consumption

Converting REST to tRPC

Before (REST):

// pages/api/users/[id].ts
export default async function handler(req, res) {
  if (req.method === 'GET') {
    const user = await db.user.findUnique({ where: { id: req.query.id } });
    res.json(user);
  } else if (req.method === 'PATCH') {
    const user = await db.user.update({
      where: { id: req.query.id },
      data: req.body,
    });
    res.json(user);
  }
}

// Client
const response = await fetch(`/api/users/${id}`);
const user = await response.json(); // No types!

After (tRPC):

// server/routers/user.ts
export const userRouter = t.router({
  getById: t.procedure
    .input(z.string())
    .query(({ input }) => db.user.findUnique({ where: { id: input } })),

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

// Client
const user = await trpc.user.getById.query(id); // Fully typed!

Shared Validation

// Reuse Zod schemas across REST and tRPC during migration
import { createUserSchema } from '../schemas/user';

// tRPC
const router = t.router({
  createUser: t.procedure
    .input(createUserSchema)
    .mutation(({ input }) => createUser(input)),
});

// REST (validate with same schema)
export default async function handler(req, res) {
  const parsed = createUserSchema.safeParse(req.body);
  if (!parsed.success) {
    return res.status(400).json({ errors: parsed.error });
  }
  const user = await createUser(parsed.data);
  res.json(user);
}

---

Best Practices & Performance

Code Organization

server/
├── trpc.ts              # tRPC instance, base procedures
├── context.ts           # Context creation
├── middleware/
│   ├── auth.ts          # Authentication middleware
│   ├── logging.ts       # Logging middleware
│   └── rateLimit.ts     # Rate limiting
├── routers/
│   ├── _app.ts          # Root router
│   ├── user.ts          # User procedures
│   ├── post.ts          # Post procedures
│   └── admin/
│       └── index.ts     # Admin-only procedures
└── schemas/
    ├── user.ts          # User Zod schemas
    └── post.ts          # Post Zod schemas

Performance Tips

  1. Use batching for multiple queries:
   httpBatchLink({ url: '/api/trpc', maxBatchSize: 10 })
  1. Implement DataLoader for N+1 queries:
   const userLoader = new DataLoader(batchLoadUsers);
  1. Cache expensive queries:
   trpc.posts.list.useQuery(undefined, { staleTime: 5 * 60 * 1000 });
  1. Optimize database queries:
   // ❌ Bad: N+1 query
   const posts = await db.post.findMany();
   const postsWithAuthors = await Promise.all(
     posts.map((p) => db.user.findUnique({ where: { id: p.authorId } }))
   );

   // ✅ Good: Single query with include
   const posts = await db.post.findMany({
     include: { author: true },
   });
  1. Use React Query's deduplication:
   // Multiple components can call same query—React Query deduplicates
   const { data } = trpc.user.getMe.useQuery();

Security Best Practices

  1. Always validate input with Zod
  2. Use middleware for authentication:
   const protectedProcedure = t.procedure.use(isAuthed);
  1. Sanitize error messages in production
  2. Implement rate limiting
  3. Use HTTPS in production
  4. Set CORS properly:
   createNextApiHandler({
     router: appRouter,
     createContext,
     onError: ({ error }) => {
       if (error.code === 'INTERNAL_SERVER_ERROR') {
         console.error('Internal error:', error);
       }
     },
   });

Type Safety Tips

  1. Export router type, not implementation:
   export type AppRouter = typeof appRouter; // ✅
   // Don't export `appRouter` itself to client
  1. Use satisfies for better inference:
   const input = {
     name: 'Alice',
     age: 30,
   } satisfies CreateUserInput;
  1. Avoid any in context:
   // ❌ Bad
   ctx: { user: any }

   // ✅ Good
   ctx: { user: User | null }

Development Workflow

  1. Define schema first: Write Zod schemas before procedures
  2. Test procedures in isolation: Use createCaller for unit tests
  3. Use TypeScript strict mode: Catch type errors early
  4. Enable React Query DevTools:
   import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

   <ReactQueryDevtools initialIsOpen={false} />

Common Pitfalls

❌ Don't return sensitive data:

// Bad: Exposes password hash
.query(() => db.user.findMany())

// Good: Select specific fields
.query(() => db.user.findMany({ select: { id: true, name: true } }))

❌ Don't use mutations for reads:

// Bad: Side-effect-free operation as mutation
getMostRecentPost: t.procedure.mutation(() => getPost())

// Good: Use query for reads
getMostRecentPost: t.procedure.query(() => getPost())

❌ Don't skip input validation:

// Bad: No validation
.input(z.any())

// Good: Strict validation
.input(z.object({ id: z.string().uuid() }))

Monitoring & Observability

const t = initTRPC.context<Context>().create({
  errorFormatter({ shape, error }) {
    // Log metrics
    metrics.increment('trpc.error', { code: error.code });

    // Send to error tracking
    if (error.code === 'INTERNAL_SERVER_ERROR') {
      Sentry.captureException(error);
    }

    return shape;
  },
});

const loggingMiddleware = t.middleware(async ({ path, type, next }) => {
  const start = Date.now();
  const result = await next();

  // Log performance metrics
  metrics.timing('trpc.duration', Date.now() - start, { path, type });

  return result;
});

---

Summary

tRPC enables type-safe APIs with minimal boilerplate:

  • ✅ No code generation: Types inferred from TypeScript
  • ✅ React Query integration: Built-in caching and optimistic updates
  • ✅ Next.js first-class support: App Router, Server Components
  • ✅ Developer experience: Auto-complete, refactoring, type errors

Best for: Full-stack TypeScript apps, Next.js projects, internal tools Avoid for: Public APIs, multi-language services

Get Started: Install → Define router → Use in client → Enjoy type safety!

Related Skills: Zod (validation), React Query (caching), Next.js (integration)

Score

0–100
63/ 100

Grade

C

Popularity15/30

1,059 installs — growing adoption.

Completeness27/30

Documented: full SKILL.md body, description, one-line install. Missing: category/license metadata.

Trust15/25

Community skill with a public GitHub source repository you can review.

Freshness6/15

No update timestamp is tracked for this skill in our catalog.

Scored automatically from popularity, completeness, trust, and freshness — computed only from data in our catalog, never fabricated.

Proud of your score? Add this badge to your README.

Paste a snippet into your GitHub README. The badge updates automatically and links back to this page.

Trpc Type Safety skill score badge previewScore badge

Markdown

[![Trpc Type Safety skill](https://www.claudemarket.ai/skills/bobmatnyc/claude-mpm-skills/trpc-type-safety/badges/score.svg)](https://www.claudemarket.ai/skills/bobmatnyc/claude-mpm-skills/trpc-type-safety)

HTML

<a href="https://www.claudemarket.ai/skills/bobmatnyc/claude-mpm-skills/trpc-type-safety"><img src="https://www.claudemarket.ai/skills/bobmatnyc/claude-mpm-skills/trpc-type-safety/badges/score.svg" alt="Trpc Type Safety skill"/></a>

Trpc Type Safety FAQ

How do I install the Trpc Type Safety skill?

Run “npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill trpc-type-safety” in your terminal. The skill is added to your agent's skills directory and picked up automatically on the next run — no restart or extra configuration needed.

What does the Trpc Type Safety skill do?

tRPC end-to-end type-safe APIs for TypeScript with React Query integration and full-stack type safety The full SKILL.md on this page shows the exact instructions the skill gives your agent.

Is the Trpc Type Safety skill free?

Yes. Trpc Type Safety is a free, open-source skill published from bobmatnyc/claude-mpm-skills. As with any third-party skill, review the source repository before installing it into an agent with sensitive access.

Does Trpc Type Safety work with Claude Code and OpenClaw?

Yes. Skills use the portable SKILL.md format, so Trpc Type Safety works with Claude Code, OpenClaw, Codex, Hermes, and any other agent that reads SKILL.md skills.

Recommended skills

Browse all →
prototype logo

prototype

mattpocock/skills

554K installsInstall
find-skills logo

find-skills

vercel-labs/skills

2.9M installsInstall
grill-me logo

grill-me

mattpocock/skills

830K installsInstall
frontend-design logo

frontend-design

anthropics/skills

767K installsInstall
grill-with-docs logo

grill-with-docs

mattpocock/skills

706K installsInstall
improve-codebase-architecture logo

improve-codebase-architecture

mattpocock/skills

681K installsInstall

Related guides

Hand-picked reading to help you choose, install, and use agent skills.

Guide10 Openclaw Skills Every Nextjs Developer NeedsGuideBest Openclaw Skills 2026GuideHow To Evaluate Openclaw Skill Before Installing

Skills by category

FrontendBackend & APIsTesting & QASecurityDevOps & CI/CDMCP & ToolingAutomationData & Analysis+27 more

MCP servers by category

MCP & ToolingBackend & APIsData & AnalysisDevOps & CI/CDAutomationSecurityDocsTesting & QA+24 more

Plugins by category

AutomationDevOps & CI/CDData & AnalysisDesign & CreativeSecurityBackend & APIsFrontendTesting & QA+16 more

Marketplaces by category

AutomationData & AnalysisDevOps & CI/CDDesign & CreativeFrontendBackend & APIsTesting & QASecurity+21 more

The Agent Stack

Weekly Claude Code, Agent SDK, and MCP moves worth your time — free.

Claude Market

AI agent skills directory, marketplace, and workflow hub for OpenClaw, Hermes Agent, Claude Code, Codex, and MCP-powered operator stacks.

Independent project, not affiliated with Anthropic.

Resources

  • Browse Skills
  • Browse MCP Servers
  • Browse Plugins
  • Browse Marketplaces
  • Newsletter

More

  • Submit a Tool
  • Create a Skill
  • Advertise
  • Free Tools
  • API
  • Shipping
  • Contact
  • Terms
  • Privacy
© 2026 Claude Market · Not affiliated with Anthropic
Fazier badgeFeatured on Twelve ToolsFeatured on Wired BusinessRemote OpenClaw - Featured on AI Agents DirectoryListed on Turbo0Featured on Uneed