Say Goodbye to API Documentation: Building End-to-End Type-Safe Applications with tRPC

4 min

1. The “Contract” Problem in Frontend–Backend Collaboration

In traditional frontend–backend development, the API is the “contract” between the two sides. We usually maintain this contract in the following ways:

  • RESTful API: Relying on tools such as OpenAPI (Swagger) to generate and maintain detailed API documentation.
  • GraphQL: Relying on a strict Schema Definition Language (SDL) to define data structures and operations.

These approaches work, but they share one problem: the contract and the implementation are separate. When a frontend developer calls an API, they trust that it will return the data structure described by the documentation or schema. If the backend implementation changes—for example, a field is renamed—but the documentation or schema is not updated in time, the mismatch will surface only at runtime and cause a bug.

Is there a way for the “contract” to stay synchronized with the implementation automatically, or even expose mismatches at compile time?

2. tRPC’s Core Idea: Share Types, Not Schemas

tRPC (TypeScript Remote Procedure Call) proposes a radical yet extremely simple approach: if both your frontend and backend use TypeScript, why not share types directly?

tRPC lets you write plain TypeScript functions as backend APIs and call them directly from the frontend with complete type inference and autocompletion, just as if you were calling a function from a local module.

It does not depend on a schema or code generation. The only “contract” is the TypeScript type itself.

3. How Does It Work?

tRPC’s magic comes from type inference and a little clever encapsulation.

a. Backend: Define an API Router

On the backend—usually a Node.js service—you use tRPC functions to create one or more “routers.” Each router is a set of callable “procedures,” which are your API endpoints.

// server/router.ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod'; // 使用 Zod 进行运行时校验

const t = initTRPC.create();

export const appRouter = t.router({
  // 定义一个名为 `getUser` 的查询 procedure
  getUser: t.procedure
    .input(z.object({ userId: z.string() }))
    .query(({ input }) => {
      // 在这里查询数据库或执行其他逻辑
      const user = { id: input.userId, name: 'Alex' };
      return user;
    }),

  // 定义一个名为 `createUser` 的变更 procedure
  createUser: t.procedure
    .input(z.object({ name: z.string() }))
    .mutation(({ input }) => {
      const user = { id: `${Math.random()}`, name: input.name };
      return user;
    }),
});

// 导出 router 的类型定义
export type AppRouter = typeof appRouter;

b. Frontend: Create a Client and Call the API

On the frontend, you only need to import AppRouter as a type from the backend. Notice that you import only a type, not any server-side code.

// client/trpc.ts
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../server/router'; // 只导入类型

export const trpc = createTRPCReact<AppRouter>();

Now you can call the API from a React component as if you were calling a local function, with complete type safety and autocompletion.

// client/components/UserInfo.tsx
import { trpc } from '../trpc';

function UserInfo({ userId }: { userId: string }) {
  // `useQuery` 的第一个参数是 procedure 的路径
  // 你输入 `trpc.` 时,IDE 会自动提示 `getUser` 和 `createUser`
  const userQuery = trpc.getUser.useQuery({ userId });

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

  // `userQuery.data` 的类型被自动推断为 { id: string; name: string }
  return <div>User: {userQuery.data?.name}</div>;
}

If the backend developer now renames the field returned by getUser from name to fullName, userQuery.data?.name in the frontend will immediately cause a TypeScript compilation error instead of being discovered at runtime.

4. Why Choose tRPC?

  • Absolute end-to-end type safety: This is its core value. It eliminates an entire class of bugs caused by mismatched API contracts.
  • Excellent developer experience: IDE autocompletion means you no longer need to consult documentation or guess the API’s structure. Refactoring becomes exceptionally easy and safe.
  • No code generation: There is no additional build step, so the feedback loop is extremely fast.
  • Lightweight and flexible: tRPC itself is very small and can integrate with any frontend framework and backend service.

Conclusion

tRPC brings unprecedented fluidity to full-stack TypeScript development. By eliminating the dependency on API documentation and schemas, it makes collaboration between frontend and backend seamless and exceptionally safe. Its advantages become even more pronounced in a monorepo architecture. If you are building a full-stack TypeScript application, tRPC is a revolutionary tool well worth your time.