
tRPC là gì? Framework API end-to-end typesafe cho TypeScript giúp loại bỏ hoàn toàn lớp API truyền thống bằng cách chia sẻ kiểu dữ liệu giữa client và server. Khác với REST hay GraphQL, tRPC không cần code generation hay build step — TypeScript tự suy luận kiểu từ server sang client ngay lập tức.
Bài viết này giải thích tRPC hoạt động thế nào, khi nào nên dùng, so sánh với REST/GraphQL và hướng dẫn setup cơ bản với Next.js.

tRPC hoạt động như thế nào?
tRPC (TypeScript Remote Procedure Call) dựa trên ý tưởng đơn giản: server và client chia sẻ cùng một định nghĩa kiểu TypeScript. Khi bạn định nghĩa router và procedure trên backend, TypeScript tự động “biết” hình dạng dữ liệu đầu vào/ra ở frontend mà không cần schema riêng hay codegen.
Quy trình cốt lõi:
- Định nghĩa router trên server với
initTRPC, gồm các procedure (query, mutation, subscription). - Export type AppRouter — chỉ type, không phải implementation.
- Client import type và tạo
createTRPCClient()— autocomplete hoạt động ngay. - Gọi procedure như hàm cục bộ:
trpc.greeting.query({ name: 'John' })— type-safe hoàn toàn.
Khác với GraphQL cần codegen hoặc REST cần OpenAPI spec + client generation, tRPC dùng TypeScript inference thuần túy. Không có build step, không có runtime overhead, bundle size cực nhỏ (~1KB client).

So sánh tRPC vs REST vs GraphQL
| Tiêu chí | tRPC | REST | GraphQL |
|---|---|---|---|
| Type safety | End-to-end tự động | Cần OpenAPI + codegen | Cần schema + codegen |
| Build step | Không | Có (nếu gen client) | Có (codegen) |
| Bundle size (client) | ~1KB | Tùy client lib | ~10-50KB (Apollo/urql) |
| Over-fetching/under-fetching | Không (gọi procedure chính xác) | Phổ biến | Giải quyết được |
| Caching | React Query built-in | HTTP cache chuẩn | Normalized cache phức tạp |
| Phù hợp | Monorepo, full-stack TS, team nhỏ | Public API, multi-client | Public API, query phức tạp |
tRPC tỏa sáng khi bạn kiểm soát cả client và server (monorepo Next.js, Expo, SvelteKit). Nếu API public cho third-party, REST/GraphQL vẫn phù hợp hơn.
Setup tRPC với Next.js App Router
Cài đặt:
npm install @trpc/server @trpc/client @trpc/react-query @trpc/next superjson zod @tanstack/react-query
Tạo server/trpc.ts (init tRPC):
import { initTRPC, TRPCError } from '@trpc/server';
import { ZodError } from 'zod';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import SuperJSON from 'superjson';
export const createTRPCContext = async () => {
const session = await getServerSession(authOptions);
return { session };
};
const t = initTRPC.context().create({
transformer: SuperJSON,
errorFormatter: ({ shape, error }) => ({
...shape,
data: { ...shape.data, zodError: error.cause instanceof ZodError ? error.cause.flatten() : null },
}),
});
export const createTRPCRouter = t.router;
export const publicProcedure = t.procedure;
export const protectedProcedure = t.procedure.use(async ({ ctx, next }) => {
if (!ctx.session) throw new TRPCError({ code: 'UNAUTHORIZED' });
return next({ ctx: { session: ctx.session } });
});
Tạo router server/routers/app.ts:
import { z } from 'zod';
import { createTRPCRouter, publicProcedure, protectedProcedure } from '@/server/trpc';
export const appRouter = createTRPCRouter({
greeting: publicProcedure
.input(z.object({ name: z.string() }))
.query(({ input }) => `Hello, ${input.name}!`),
getProfile: protectedProcedure.query(async ({ ctx }) => {
// ctx.session đã có user
return { id: ctx.session.user.id, name: ctx.session.user.name };
}),
updateName: protectedProcedure
.input(z.object({ name: z.string().min(2) }))
.mutation(async ({ ctx, input }) => {
// update DB
return { success: true };
}),
});
export type AppRouter = typeof appRouter;
Tạo Next.js handler app/api/trpc/[trpc]/route.ts:
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from '@/server/routers/app';
import { createTRPCContext } from '@/server/trpc';
const handler = (req: Request) =>
fetchRequestHandler({
endpoint: '/api/trpc',
req,
router: appRouter,
createContext: createTRPCContext,
});
export { handler as GET, handler as POST };
Client provider components/trpc-provider.tsx:
'use client';
import { createTRPCReact } from '@trpc/react-query';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import SuperJSON from 'superjson';
import { useState } from 'react';
import type { AppRouter } from '@/server/routers/app';
const trpc = createTRPCReact();
export function TRPCProvider({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => new QueryClient({
defaultOptions: { queries: { staleTime: 60_000 } },
}));
const [trpcClient] = useState(() => trpc.createClient({
links: [httpBatchLink({ url: '/api/trpc', transformer: SuperJSON })],
}));
return (
{children}
);
}
Dùng trong component:
import { trpc } from '@/utils/trpc';
export function Greeting() {
const { data } = trpc.greeting.useQuery({ name: 'tRPC' });
return {data ?? 'Loading...'};
}
Khi nào nên chọn tRPC?
- ✓ Dùng tRPC khi: Full-stack TypeScript, monorepo, team nhỏ-trung bình, muốn DX tối ưu, ít boilerplate.
- ✗ Tránh tRPC khi: API public cho third-party, nhiều client ngôn ngữ khác nhau, cần caching phức tạp như GraphQL, team đã quen REST/GraphQL và không muốn chuyển.
Hệ sinh thái và công cụ hỗ trợ
- tRPC Docs — Tài liệu chính thức
- GitHub tRPC — Source code & issues
- Awesome tRPC — Boilerplate, examples, tools
- TanStack Query — Caching/sync layer tRPC dùng mặc định
- Zod — Schema validation tích hợp sẵn
tRPC đang trở thành lựa chọn mặc định cho các dự án full-stack TypeScript hiện đại nhờ DX vượt trội và zero-overhead. Nếu bạn đang xây dựng ứng dụng Next.js/React/Expo với TypeScript, hãy thử tRPC — nó có thể loại bỏ hoàn toàn các file OpenAPI, codegen script và client wrapper thủ công.
