Skills DirectorySkills Directory
SkillsLearnSecurityCategoriesDocsBlogPro
Sign InSubmit Skill
Skills Directory

Security-tested agent skills for Claude, coding agents, and AI workflows.

Directory

  • Browse Skills
  • All Skills A–Z
  • Claude Skills
  • Claude Code Skills
  • Agent Skills
  • Categories
  • Authors
  • Submit a Skill

Learn

  • Learn Hub
  • Install Claude Skills
  • Write SKILL.md
  • Skills vs MCP
  • Directories Compared

Security

  • Security
  • Methodology
  • Secure Claude Skills
  • Security Badges
  • Chrome Extension
  • Skill Manager

Company

  • About
  • Community
  • Blog
  • API Docs
  • Advertise

2026 Skills Directory. All rights reserved.

ProTermsPrivacyRefunds
Back to skills

Web Data Fetching Trpc

ASecurity

tRPC type-safe API patterns — routers and procedures, input validation, context and middleware, TRPCError, and the typed client bridge

24 stars
0 votes
0 copies
0 views
Added 5/29/2026
ai-agentstypescriptgoreactapi

Works with

cursorcliapi

Security Analysis

A100/100

Pro scans all 8 files and shows the line behind each finding

Scanned 9/21/2026

$npx -y skills add agents-inc/skills --skill web-data-fetching-trpc --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Web Data Fetching Trpc?

Add the live security badge to your README — it updates automatically with every re-scan.

Security grade badge for Web Data Fetching Trpc
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/agents-inc-web-data-fetching-trpc/badge)](https://www.skillsdirectory.com/skills/agents-inc-web-data-fetching-trpc)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
Files
SKILL.md
---
name: web-data-fetching-trpc
description: tRPC type-safe API patterns — routers and procedures, input validation, context and middleware, TRPCError, and the typed client bridge
---

# tRPC Patterns

> **Quick Guide:** tRPC carries types from server to client through one exported type rather than
> through a generated schema, so `export type AppRouter = typeof appRouter` is the whole bridge and
> everything downstream fails without it. Procedures take a validated input and return a value;
> `TRPCError` codes are what become HTTP statuses; middleware narrows the context type so an
> authenticated procedure's `ctx.user` is non-nullable. In v11 the transformer moved inside the
> link, subscriptions are async generators, and `@trpc/tanstack-react-query` is the current React
> integration.

**Detailed Resources:**

- [examples/core.md](examples/core.md) — initialization, context, a CRUD router, the provider, inferred types, `queryOptions`
- [examples/middleware.md](examples/middleware.md) — logging, rate limiting, resource-scoped access
- [examples/infinite-queries.md](examples/infinite-queries.md) — cursor pagination end to end
- [examples/optimistic-updates.md](examples/optimistic-updates.md) — the full snapshot-and-rollback cycle
- [examples/subscriptions.md](examples/subscriptions.md) — async generator subscriptions with resumable event ids
- [examples/file-uploads.md](examples/file-uploads.md) — `File` in an input schema (v11+)
- [reference.md](reference.md) — error code to HTTP status table, batching and invalidation notes, v10 → v11 migration

---

## Which path applies

- **`@trpc/tanstack-react-query`** — the current integration. `createTRPCContext` yields a `useTRPC`
  hook, and each procedure exposes `queryOptions()`, `mutationOptions()`, `infiniteQueryOptions()`
  and `queryKey()` that go straight into the standard query hooks. Pattern 5.
- **`@trpc/react-query`** — the classic integration, still supported in v11. Procedures carry their
  own `trpc.x.useQuery()` hooks instead, and a cache key comes from `getQueryKey(trpc.x)` rather
  than from a `queryKey()` on the procedure. Migrate when convenient; the two can coexist.

---

<critical_requirements>

## Before writing tRPC code

**Export the router's type: `export type AppRouter = typeof appRouter`.** That single line is the
whole client-side contract — without it the client falls back to `unknown` and every guarantee tRPC
offers is gone, with no error at the point the export was forgotten.

**Give every procedure that accepts input a validator on `.input()`.** It is both the runtime check
and the source of the handler's parameter type, so a procedure without one receives `unknown` and
tempts a cast.

**Throw `TRPCError` with a code rather than a bare `Error`.** The code is what maps to an HTTP
status and what the client switches on; a bare `Error` arrives as an opaque 500.

**Place the transformer inside `httpBatchLink()`, not on `createTRPCClient()`.** v11 moved it, and
the old position raises an error at client construction.

</critical_requirements>

---

**Auto-detection:** `initTRPC`, `createTRPCClient`, `createTRPCContext`, `createTRPCOptionsProxy`,
`@trpc/server`, `@trpc/client`, `@trpc/react-query`, `@trpc/tanstack-react-query`, `TRPCError`,
`publicProcedure`, `protectedProcedure`, `httpBatchLink`, `httpSubscriptionLink`, `loggerLink`,
`inferRouterInputs`, `inferRouterOutputs`, `useTRPC`, `tracked`, `AppRouter`

**Applies to:**

- Router and procedure definition, and composing routers into one
- Input validation and the types inferred from it
- Per-request context, and middleware that narrows it
- Error codes and the shape the client receives
- Turning a procedure into typed query and mutation options
- Subscriptions, file inputs and cursor pagination

**Handled elsewhere:**

- APIs published for third-party or non-TypeScript consumers, which want a language-neutral contract
  document rather than a shared type
- Caching policy, retries and invalidation semantics — this skill settles how a procedure becomes
  the options a query client consumes, and the client decides what to do with them
- The schema library used on `.input()`; any validator the version supports works, and the examples
  show one
- Session and token issuance; context consumes a session rather than establishing one

---

<philosophy>

## Philosophy

There is no API description anywhere — no schema file, no generated client, no build step between
the two halves. The router's inferred type _is_ the contract, and it is shared by importing a type
across the codebase.

That buys immediate accuracy: a procedure's return type changes and every call site reddens in the
same typecheck, with no regeneration step to forget. It costs polyglot support, a published
contract, and HTTP caching — calls go out as POST by default, so a CDN in front of the endpoint has
nothing to cache. Which is why tRPC suits an internal API in one TypeScript codebase and suits a
public one badly.

</philosophy>

---

<patterns>

## Core patterns

### Pattern 1: Initialization

Initialize once per application and export the factories the routers compose from.

```typescript
const t = initTRPC.context<Context>().create({
  transformer: superjson, // Date, Map and Set survive the wire
  errorFormatter({ shape, error }) {
    return {
      ...shape,
      data: {
        ...shape.data,
        zodError:
          error.cause instanceof ZodError ? error.cause.flatten() : null,
      },
    };
  },
});

export const router = t.router;
export const publicProcedure = t.procedure;
export const middleware = t.middleware;
```

The error formatter is what turns a validation failure into something a form can render per field,
instead of one message.

Full code: [examples/core.md](examples/core.md) — including the per-request context factory

---

### Pattern 2: Procedures and input validation

```typescript
const createUserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(1).max(100),
});

export const userRouter = router({
  create: protectedProcedure
    .input(createUserSchema)
    .mutation(async ({ input, ctx }) => {
      // input: { email: string; name: string } — validated, and typed from the same schema
      return ctx.db.user.create({ data: input });
    }),
});
```

Full code: [examples/core.md](examples/core.md)

---

### Pattern 3: Middleware that narrows the context

```typescript
const isAuthenticated = middleware(async ({ ctx, next }) => {
  if (!ctx.session || !ctx.user) throw new TRPCError({ code: "UNAUTHORIZED" });
  return next({ ctx: { ...ctx, session: ctx.session, user: ctx.user } });
});

export const protectedProcedure = publicProcedure.use(isAuthenticated);
```

The `next({ ctx })` return type is what makes `ctx.user` non-nullable in every procedure built on
`protectedProcedure` — so forgetting the check becomes a compile error rather than a review comment.

Full code: [examples/middleware.md](examples/middleware.md)

---

### Pattern 4: The type bridge

```typescript
export const appRouter = router({ user: userRouter, post: postRouter });
export type AppRouter = typeof appRouter;

type RouterInputs = inferRouterInputs<AppRouter>;
type RouterOutputs = inferRouterOutputs<AppRouter>;

type User = RouterOutputs["user"]["getById"];
```

Component props take `RouterOutputs[...]` rather than a hand-written interface, so a field removed
from a procedure reddens every component that read it.

Full code: [examples/core.md](examples/core.md)

---

### Pattern 5: Typed query and mutation options

```typescript
export const { TRPCProvider, useTRPC } = createTRPCContext<AppRouter>();

const trpc = useTRPC();
const { data } = useQuery(trpc.user.getById.queryOptions({ id: userId }));
```

Each procedure carries `queryOptions()`, `mutationOptions()`, `infiniteQueryOptions()` and
`queryKey()`. The options object goes into the ordinary hook, so anything the query client can do to
an options object works here too.

Full code: [examples/core.md](examples/core.md) — provider, links and a component

---

### Pattern 6: Errors

```typescript
// Server
throw new TRPCError({ code: "NOT_FOUND", message: "User not found" });
throw new TRPCError({
  code: "INTERNAL_SERVER_ERROR",
  message: "Failed to delete",
  cause: error,
});
```

```typescript
// Client
onError: (error) => {
  switch (error.data?.code) {
    case "NOT_FOUND":
      return toast.error("Not found");
    case "FORBIDDEN":
      return toast.error("Not allowed");
  }
};
```

`cause` keeps the original stack for the server logs while the client still receives only the code
and message. The code-to-status table is in [reference.md](reference.md).

---

### Pattern 7: Optimistic updates

Cancel, snapshot, write, roll back on failure, invalidate when settled — all five, since the
rollback is what makes the optimistic write safe.

```typescript
const toggleTodo = useMutation({
  ...trpc.todo.toggle.mutationOptions(),
  onMutate: async ({ id }) => {
    await queryClient.cancelQueries({ queryKey: trpc.todo.list.queryKey() });
    const previousTodos = queryClient.getQueryData(trpc.todo.list.queryKey());
    queryClient.setQueryData(trpc.todo.list.queryKey(), (old) =>
      toggle(old, id),
    );
    return { previousTodos };
  },
  onError: (err, vars, context) =>
    queryClient.setQueryData(trpc.todo.list.queryKey(), context?.previousTodos),
  onSettled: () =>
    queryClient.invalidateQueries({ queryKey: trpc.todo.list.queryKey() }),
});
```

The `cancelQueries` is not optional: an in-flight refetch that resolves after the optimistic write
overwrites it with the pre-mutation server state.

Full code: [examples/optimistic-updates.md](examples/optimistic-updates.md)

</patterns>

---

<red_flags>

## Red flags

**Breaks at runtime:**

- No `export type AppRouter` — the client infers `unknown` and every procedure call is untyped,
  with nothing failing at the file that omitted it.
- A transformer on `createTRPCClient()` in v11 — client construction throws, naming the link it
  should have gone in.
- A transformer configured on one side only — the two ends disagree about the wire format, and
  `Date`, `Map` and `Set` arrive as something else.
- `observable()` in a subscription — that is the v10 shape; v11 subscriptions are async generators.
- `rawInput` in middleware — v11 replaced it with `await getRawInput()`.
- An optimistic write with no snapshot and no `onError` — a failed mutation leaves the UI showing a
  change that never happened.

**Surprising behaviour:**

- `httpBatchLink` merges concurrent calls into one request, so every procedure in a batch shares one
  HTTP status — a 401 from one is the status the others see too.
- Context is built per request, so anything mutable stored on it is discarded and never shared.
- Middleware runs in declaration order, which decides what each layer can see: auth before rate
  limiting means the limit is keyed on a known user.
- A retried mutation repeats a write — set `retry: false` for mutations.
- Resumable subscriptions need `lastEventId` in the input schema for `tracked()` to have somewhere
  to resume from.
- An auth check written in a procedure body leaves `ctx.user` nullable to the type system, so the
  next procedure that forgets it compiles.

</red_flags>

Attribution

agents-incagents-inc
View sourceSee grades on GitHubMore from agents-inc →
SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments (0)

No comments yet. Be the first to comment!

SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Related Skills

Caveman

Terse caveman voice: answer first, fluff gone, every technical fact kept. Use for /caveman, "caveman mode", "talk like caveman", "be brief", "less tokens". Stays on until "stop caveman" or "normal mode".

1100021 votes

Hyperplan

Adversarial multi-agent planning skill. Self-orchestrates 5 hostile category members (unspecified-low, unspecified-high, deep, ultrabrain, artistry) via team-mode for ruthless cross-critique debate, distills only the defensible insights, then MANDATORILY hands the distilled insight bundle to the `plan` agent for executable plan formalization. Use when planning needs maximum rigor and surfacing of weak assumptions, blind spots, and over-engineering. Triggers: 'hyperplan', 'hpp', '/hyperplan', ...

698621 votes

Writing Skills

Create and manage Claude Code skills in HASH repository following Anthropic best practices. Use when creating new skills, modifying skill-rules.json, understanding trigger patterns, working with hooks, debugging skill activation, or implementing progressive disclosure. Covers skill structure, YAML frontmatter, trigger types (keywords, intent patterns), UserPromptSubmit hook, and the 500-line rule. Includes validation and debugging with SKILL_DEBUG. Examples include rust-error-stack, cargo-dep...

3931 votes

Mcp Code Execution

Routes multi-tool workflows through MCP servers for large datasets and pipelines. Use when Bash tool overhead is limiting throughput on data-heavy tasks.

3421 votes

catchup

Recovers the conversation and failed tool calls of a previous Codex, Amp, Claude Code, Antigravity, Cline, Copilot CLI, Cursor, DeepSeek Harness, Grok Build, Kimi, OpenCode, Pi Agent, or ZCode session. Use when the user says "catch up", "what did the last session do", "get me up to speed", "I switched agents", asks to recover/summarize a previous session before continuing, or asks to diagnose or report a catchup failure. Do NOT use for the current conversation, git history, or any non-agent log.

741 votes
View all in ai-agents →