ScriptsApr 11, 2026·1 min read

tRPC — End-to-End Typesafe APIs for TypeScript

tRPC lets you build fully typesafe APIs without schemas, code generation, or runtime bloat. Share types between frontend and backend automatically. Works with Next.js, React, Vue, and more.

SC
Script Depot · Community
Quick Use

Use it first, then decide how deep to go

This block should tell both the user and the agent what to copy, install, and apply first.

# Install in your Next.js project
npm install @trpc/server @trpc/client @trpc/react-query @trpc/next @tanstack/react-query zod

# Create router
# server/trpc.ts
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
export const router = t.router;
export const publicProcedure = t.procedure;
Intro

tRPC allows you to easily build and consume fully typesafe APIs without schemas, code generation, or runtime bloat. It leverages TypeScript inference to automatically share types between your server and client — meaning every API call is type-checked end-to-end, with autocomplete and instant feedback on breaking changes.

With 40K+ GitHub stars and MIT license, tRPC has revolutionized full-stack TypeScript development, eliminating the need for REST/GraphQL schemas and code generation in monorepo projects.

What tRPC Does

  • End-to-End Types: Share TypeScript types between client and server automatically
  • No Schema: No protobuf, GraphQL schemas, or OpenAPI to maintain
  • Input Validation: Built-in Zod/Yup/Valibot validation
  • Subscriptions: WebSocket support for real-time updates
  • Middleware: Reusable logic for auth, logging, rate limiting
  • Error Handling: Typed error responses
  • Batching: Automatic request batching to reduce round-trips
  • Framework Support: Next.js, Express, Fastify, AWS Lambda, Cloudflare Workers
  • Client Support: React, Vue, vanilla TS, Svelte, SolidJS
  • Streaming: Server-sent events for streamed responses

How It Works

Backend (TypeScript)          Frontend (TypeScript)
──────────────────            ──────────────────
1. Define procedures   ──▶    4. Import types
2. Define inputs              5. Call with autocomplete
3. Return data                6. Full type safety

No schemas. No codegen. Just TypeScript.

Example: Next.js App

1. Define Router (server)

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

const t = initTRPC.create();
export const router = t.router;
export const publicProcedure = t.procedure;
// server/routers/_app.ts
import { z } from 'zod';
import { router, publicProcedure } from '../trpc';

export const appRouter = router({
  // Query (GET equivalent)
  getUser: publicProcedure
    .input(z.object({ id: z.string() }))
    .query(async ({ input }) => {
      const user = await db.user.findUnique({ where: { id: input.id } });
      return user;
    }),

  // Mutation (POST/PUT/DELETE equivalent)
  createPost: publicProcedure
    .input(z.object({
      title: z.string().min(1).max(100),
      content: z.string().min(10),
      published: z.boolean().default(false),
    }))
    .mutation(async ({ input }) => {
      const post = await db.post.create({ data: input });
      return post;
    }),

  // List with pagination
  listPosts: publicProcedure
    .input(z.object({
      limit: z.number().min(1).max(100).default(10),
      cursor: z.string().nullish(),
    }))
    .query(async ({ input }) => {
      const posts = await db.post.findMany({
        take: input.limit + 1,
        cursor: input.cursor ? { id: input.cursor } : undefined,
        orderBy: { createdAt: 'desc' },
      });

      let nextCursor: typeof input.cursor = undefined;
      if (posts.length > input.limit) {
        const nextItem = posts.pop();
        nextCursor = nextItem?.id;
      }

      return { posts, nextCursor };
    }),
});

// Export type for client
export type AppRouter = typeof appRouter;

2. Client Setup

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

export const trpc = createTRPCReact<AppRouter>();
// pages/_app.tsx
import { httpBatchLink } from '@trpc/client';
import { trpc } from '../lib/trpc';

function MyApp({ Component, pageProps }) {
  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}>
        <Component {...pageProps} />
      </QueryClientProvider>
    </trpc.Provider>
  );
}

3. Use in Components (Full Type Safety!)

// components/UserProfile.tsx
import { trpc } from '../lib/trpc';

export function UserProfile({ userId }: { userId: string }) {
  // Fully typed query - autocomplete for 'getUser'
  const { data: user, isLoading } = trpc.getUser.useQuery({ id: userId });

  // Fully typed mutation
  const createPost = trpc.createPost.useMutation({
    onSuccess: () => {
      // Auto-refetch user data
      utils.getUser.invalidate({ id: userId });
    },
  });

  if (isLoading) return <div>Loading...</div>;

  return (
    <div>
      <h1>{user?.name}</h1>
      <button
        onClick={() => createPost.mutate({
          title: 'New Post',
          content: 'Content here',
          published: true,
        })}
      >
        Create Post
      </button>
    </div>
  );
}

Magic: If you rename a field in the backend, TypeScript immediately shows errors in every component using that field.

Key Features

Middleware

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

// Auth middleware
const authMiddleware = t.middleware(async ({ ctx, next }) => {
  if (!ctx.user) {
    throw new TRPCError({ code: 'UNAUTHORIZED' });
  }
  return next({
    ctx: { ...ctx, user: ctx.user }, // Type narrowed to non-null
  });
});

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

// Use in router
export const appRouter = router({
  getMyPosts: protectedProcedure.query(({ ctx }) => {
    return db.post.findMany({ where: { userId: ctx.user.id } });
  }),
});

Subscriptions (WebSocket)

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

const ee = new EventEmitter();

export const appRouter = router({
  onNewMessage: publicProcedure.subscription(() => {
    return observable<{ text: string }>((emit) => {
      const handler = (data: { text: string }) => emit.next(data);
      ee.on('message', handler);
      return () => ee.off('message', handler);
    });
  }),
});

// Client
trpc.onNewMessage.useSubscription(undefined, {
  onData: (message) => {
    console.log('New message:', message.text);
  },
});

Batching

// Multiple queries in one HTTP request
httpBatchLink({
  url: '/api/trpc',
  maxURLLength: 2083, // Auto-batch when possible
});
// These three queries make 1 HTTP request instead of 3
const user = trpc.user.getById.useQuery({ id: '1' });
const posts = trpc.post.list.useQuery({ userId: '1' });
const stats = trpc.stats.getUser.useQuery({ id: '1' });

tRPC vs Alternatives

Feature tRPC GraphQL REST gRPC
Type safety End-to-end Via codegen Manual Via codegen
Schema TypeScript SDL OpenAPI Protobuf
Setup Very simple Moderate Simple Complex
Client generation Not needed Required Optional Required
Runtime overhead None Resolver layer None Protobuf parser
Subscriptions Yes (WS) Yes (WS) SSE Streaming
Language support TypeScript only Many Any Many
Best for TS monorepos Multi-language Public APIs Microservices

Framework Integrations

// Next.js
import { createNextApiHandler } from '@trpc/server/adapters/next';
export default createNextApiHandler({ router: appRouter });

// Express
import { createExpressMiddleware } from '@trpc/server/adapters/express';
app.use('/trpc', createExpressMiddleware({ router: appRouter }));

// Fastify
import { fastifyTRPCPlugin } from '@trpc/server/adapters/fastify';
server.register(fastifyTRPCPlugin, { prefix: '/trpc', trpcOptions: { router } });

// Cloudflare Workers
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
export default {
  fetch(request: Request) {
    return fetchRequestHandler({ endpoint: '/trpc', req: request, router: appRouter });
  },
};

常见问题

Q: tRPC 和 GraphQL 怎么选? A: 如果你用纯 TypeScript 且前后端在同一个 monorepo,tRPC 更简单更快。如果你需要多语言客户端、复杂查询灵活性或已有 GraphQL 生态,选 GraphQL。tRPC 适合 80% 的内部应用,GraphQL 适合公共 API。

Q: 类型共享怎么实现的? A: tRPC 使用 TypeScript 类型推断。你在 server 定义 router,export AppRouter 类型,client 导入这个类型。编译时 TypeScript 就知道所有 procedure 的输入输出类型——无需运行时开销或代码生成。

Q: 适合移动端吗? A: 可以用于 React Native。tRPC 也支持 vanilla TypeScript 客户端,可以在任何 TypeScript 环境使用。不支持非 TypeScript 客户端(Python/Go/Java)——如果需要,GraphQL 或 REST 更合适。

来源与致谢

Discussion

Sign in to join the discussion.
No comments yet. Be the first to share your thoughts.

Related Assets