tRPC คือ Library สำหรับสร้าง End-to-end Type-Safe API ใน TypeScript โดยไม่ต้องเขียน Schema, ไม่ต้อง Generate code, ไม่ต้องเขียน Type definitions ซ้ำระหว่าง Client กับ Server เมื่อคุณเปลี่ยน API บน Server TypeScript compiler จะบอกคุณทันทีว่า Client ตรงไหนต้องแก้ ทำให้ Development เร็วขึ้นมากและลด Runtime errors อย่างมาก
tRPC ย่อมาจาก TypeScript Remote Procedure Call สร้างโดย Alex Johansson (KATT) เปิดตัวในปี 2021 และเติบโตอย่างรวดเร็ว ปี 2026 tRPC เวอร์ชัน 11 มี Downloads มากกว่า 2 ล้านครั้ง/สัปดาห์ ใน npm เป็นทางเลือกที่ได้รับความนิยมสูงสำหรับ Full-Stack TypeScript applications
tRPC vs REST vs GraphQL
| คุณสมบัติ | REST | GraphQL | tRPC |
|---|---|---|---|
| Type Safety | ไม่มี (ต้อง generate จาก OpenAPI) | มี (ต้อง codegen) | มี (automatic, zero codegen) |
| Schema | OpenAPI/Swagger (optional) | GraphQL Schema (required) | ไม่ต้อง (TypeScript IS the schema) |
| Code Generation | ต้อง generate (openapi-generator) | ต้อง generate (graphql-codegen) | ไม่ต้อง |
| Boilerplate | ปานกลาง | สูง (schema, resolvers, types) | ต่ำมาก |
| Learning Curve | ต่ำ | สูง | ต่ำ (ถ้ารู้ TypeScript) |
| Over/Under-fetching | มีปัญหา | แก้ได้ (query เฉพาะ field ที่ต้องการ) | ปานกลาง (ใช้ select ได้) |
| Ecosystem | ใหญ่มาก (ทุกภาษา) | ใหญ่ (ทุกภาษา) | TypeScript only |
| Client Support | ทุกภาษา ทุก Platform | ทุกภาษา ทุก Platform | TypeScript/JavaScript only |
| Best For | Public API, Multi-language | Complex data requirements, Mobile | Full-Stack TypeScript apps |
tRPC Architecture
Router
Router คือ Container ที่รวม Procedures ทั้งหมดไว้ คล้ายกับ Express Router หรือ GraphQL Schema เป็นจุดเริ่มต้นที่ Client จะเรียกใช้
Procedure
Procedure คือ Function ที่ Client เรียก มี 3 ประเภท:
query— อ่านข้อมูล (เหมือน GET / GraphQL Query)mutation— เปลี่ยนแปลงข้อมูล (เหมือน POST/PUT/DELETE / GraphQL Mutation)subscription— รับข้อมูล Real-time (WebSocket)
Context
Context คือ Object ที่ทุก Procedure เข้าถึงได้ มักใช้สำหรับ Authentication (user session), Database connection, Logger
// server/trpc.ts — Initialize tRPC
import { initTRPC, TRPCError } from '@trpc/server';
import type { Context } from './context';
import superjson from 'superjson';
const t = initTRPC.context<Context>().create({
transformer: superjson, // รองรับ Date, Map, Set, BigInt
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 protectedProcedure = t.procedure.use(async ({ ctx, next }) => {
if (!ctx.session || !ctx.session.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return next({
ctx: { ...ctx, user: ctx.session.user },
});
});
Queries และ Mutations
// server/routers/user.ts
import { z } from 'zod';
import { router, publicProcedure, protectedProcedure } from '../trpc';
export const userRouter = router({
// Query: ดึงข้อมูล User ตาม ID
getById: publicProcedure
.input(z.object({ id: z.string().uuid() }))
.query(async ({ input, ctx }) => {
const user = await ctx.db.user.findUnique({
where: { id: input.id },
select: { id: true, name: true, email: true, avatar: true },
});
if (!user) throw new TRPCError({ code: 'NOT_FOUND' });
return user;
}),
// Query: ค้นหา Users
search: publicProcedure
.input(z.object({
query: z.string().min(1).max(100),
page: z.number().int().positive().default(1),
limit: z.number().int().min(1).max(100).default(20),
}))
.query(async ({ input, ctx }) => {
const { query, page, limit } = input;
const [users, total] = await Promise.all([
ctx.db.user.findMany({
where: { name: { contains: query, mode: 'insensitive' } },
skip: (page - 1) * limit,
take: limit,
}),
ctx.db.user.count({
where: { name: { contains: query, mode: 'insensitive' } },
}),
]);
return { users, total, page, totalPages: Math.ceil(total / limit) };
}),
// Mutation: อัปเดต Profile
updateProfile: protectedProcedure
.input(z.object({
name: z.string().min(2).max(50).optional(),
bio: z.string().max(500).optional(),
avatar: z.string().url().optional(),
}))
.mutation(async ({ input, ctx }) => {
const updated = await ctx.db.user.update({
where: { id: ctx.user.id },
data: input,
});
return updated;
}),
// Mutation: ลบ Account
deleteAccount: protectedProcedure
.mutation(async ({ ctx }) => {
await ctx.db.user.delete({ where: { id: ctx.user.id } });
return { success: true };
}),
});
Input Validation ด้วย Zod
tRPC ใช้ Zod เป็น Default สำหรับ Input validation Zod schema ทำหน้าที่ทั้ง Runtime validation และ Type inference:
import { z } from 'zod';
// Schema ที่ซับซ้อน
const createPostSchema = z.object({
title: z.string()
.min(5, "Title ต้องมีอย่างน้อย 5 ตัวอักษร")
.max(200, "Title ต้องไม่เกิน 200 ตัวอักษร"),
content: z.string().min(50),
tags: z.array(z.string()).min(1).max(10),
category: z.enum(['tech', 'lifestyle', 'news', 'tutorial']),
publishAt: z.date().optional(),
metadata: z.object({
seoTitle: z.string().max(60).optional(),
seoDescription: z.string().max(160).optional(),
canonical: z.string().url().optional(),
}).optional(),
});
// ใช้ใน Procedure
export const postRouter = router({
create: protectedProcedure
.input(createPostSchema)
.mutation(async ({ input, ctx }) => {
// input ถูก validate แล้ว + มี Type ที่ถูกต้อง
// TypeScript รู้ว่า input.title เป็น string,
// input.tags เป็น string[], etc.
return ctx.db.post.create({
data: { ...input, authorId: ctx.user.id },
});
}),
});
Middleware
Middleware ใน tRPC ทำงานเหมือน Express middleware ใช้สำหรับ Authentication, Logging, Rate limiting, Permission checking:
// Logging middleware
const loggerMiddleware = t.middleware(async ({ path, type, next }) => {
const start = Date.now();
const result = await next();
const duration = Date.now() - start;
console.log(`${type} ${path} - ${duration}ms`);
return result;
});
// Rate limiting middleware
const rateLimitMiddleware = t.middleware(async ({ ctx, next }) => {
const key = `ratelimit:${ctx.ip}`;
const count = await ctx.redis.incr(key);
if (count === 1) await ctx.redis.expire(key, 60);
if (count > 100) throw new TRPCError({ code: 'TOO_MANY_REQUESTS' });
return next();
});
// Role-based access
const adminProcedure = protectedProcedure.use(async ({ ctx, next }) => {
if (ctx.user.role !== 'ADMIN') {
throw new TRPCError({ code: 'FORBIDDEN', message: 'Admin only' });
}
return next();
});
Context — Authentication
// server/context.ts
import { getServerSession } from 'next-auth';
import { authOptions } from './auth';
import { prisma } from './db';
import type { CreateNextContextOptions } from '@trpc/server/adapters/next';
export async function createContext(opts: CreateNextContextOptions) {
const session = await getServerSession(opts.req, opts.res, authOptions);
return {
session,
db: prisma,
ip: opts.req.headers['x-forwarded-for'] ?? opts.req.socket.remoteAddress,
};
}
export type Context = Awaited<ReturnType<typeof createContext>>;
Subscriptions — WebSocket Real-time
import { observable } from '@trpc/server/observable';
export const chatRouter = router({
onNewMessage: publicProcedure
.input(z.object({ roomId: z.string() }))
.subscription(({ input, ctx }) => {
return observable<Message>((emit) => {
const onMessage = (data: Message) => {
if (data.roomId === input.roomId) {
emit.next(data);
}
};
// Subscribe to event emitter / Redis Pub-Sub
ctx.ee.on('newMessage', onMessage);
return () => {
ctx.ee.off('newMessage', onMessage);
};
});
}),
sendMessage: protectedProcedure
.input(z.object({
roomId: z.string(),
text: z.string().min(1).max(5000),
}))
.mutation(async ({ input, ctx }) => {
const message = await ctx.db.message.create({
data: { ...input, userId: ctx.user.id },
});
ctx.ee.emit('newMessage', message);
return message;
}),
});
tRPC กับ Next.js App Router
// app/api/trpc/[trpc]/route.ts — API Route Handler
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from '@/server/routers/_app';
import { createContext } from '@/server/context';
const handler = (req: Request) =>
fetchRequestHandler({
endpoint: '/api/trpc',
req,
router: appRouter,
createContext,
});
export { handler as GET, handler as POST };
// Server Component — เรียก tRPC โดยตรง (ไม่ผ่าน HTTP!)
// app/users/[id]/page.tsx
import { createCaller } from '@/server/routers/_app';
import { createContext } from '@/server/context';
export default async function UserPage({ params }: { params: { id: string } }) {
const ctx = await createContext();
const caller = createCaller(ctx);
const user = await caller.user.getById({ id: params.id });
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
tRPC กับ React Query
// Client Component — ใช้ tRPC + React Query hooks
'use client';
import { trpc } from '@/utils/trpc';
export function UserProfile({ userId }: { userId: string }) {
// Query — auto type-safe!
const { data: user, isLoading, error } = trpc.user.getById.useQuery(
{ id: userId },
{ staleTime: 5 * 60 * 1000 } // cache 5 min
);
// Mutation
const updateProfile = trpc.user.updateProfile.useMutation({
onSuccess: () => {
// Invalidate cache หลัง update
trpc.useUtils().user.getById.invalidate({ id: userId });
},
});
// Infinite Query (pagination)
const { data: posts, fetchNextPage, hasNextPage } =
trpc.post.list.useInfiniteQuery(
{ limit: 10 },
{ getNextPageParam: (lastPage) => lastPage.nextCursor }
);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h1>{user.name}</h1>
<button onClick={() => updateProfile.mutate({ name: 'New Name' })}>
Update Name
</button>
</div>
);
}
tRPC + Prisma Stack
// ตัวอย่าง Full Stack: tRPC + Prisma + Next.js
// prisma/schema.prisma
// model User {
// id String @id @default(cuid())
// name String
// email String @unique
// posts Post[]
// createdAt DateTime @default(now())
// }
// server/routers/_app.ts — Root Router
import { router } from '../trpc';
import { userRouter } from './user';
import { postRouter } from './post';
import { chatRouter } from './chat';
export const appRouter = router({
user: userRouter,
post: postRouter,
chat: chatRouter,
});
export type AppRouter = typeof appRouter;
// AppRouter type ถูก export ไปใช้ที่ Client
// ทำให้ Client รู้ Type ทุก Procedure โดยไม่ต้อง codegen!
Error Handling
import { TRPCError } from '@trpc/server';
// Server-side error handling
export const postRouter = router({
getById: publicProcedure
.input(z.object({ id: z.string() }))
.query(async ({ input, ctx }) => {
try {
const post = await ctx.db.post.findUnique({
where: { id: input.id },
});
if (!post) {
throw new TRPCError({
code: 'NOT_FOUND',
message: `Post ${input.id} not found`,
});
}
if (post.status === 'DRAFT' && post.authorId !== ctx.user?.id) {
throw new TRPCError({
code: 'FORBIDDEN',
message: 'Cannot view draft posts of other users',
});
}
return post;
} catch (error) {
if (error instanceof TRPCError) throw error;
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Something went wrong',
cause: error,
});
}
}),
});
// Client-side error handling
// const { error } = trpc.post.getById.useQuery({ id: '123' });
// if (error?.data?.code === 'NOT_FOUND') { /* show 404 */ }
// if (error?.data?.code === 'UNAUTHORIZED') { /* redirect to login */ }
// tRPC Error Codes:
// BAD_REQUEST, UNAUTHORIZED, FORBIDDEN, NOT_FOUND,
// METHOD_NOT_SUPPORTED, TIMEOUT, CONFLICT,
// PRECONDITION_FAILED, PAYLOAD_TOO_LARGE,
// UNPROCESSABLE_CONTENT, TOO_MANY_REQUESTS,
// CLIENT_CLOSED_REQUEST, INTERNAL_SERVER_ERROR
tRPC v11 Features
tRPC v11 (2025-2026) มาพร้อม Features ใหม่ที่สำคัญ:
FormData support: รองรับ File uploads ด้วย FormData โดยไม่ต้องใช้ Library เพิ่ม
Server-Sent Events (SSE): รองรับ SSE เป็น Transport สำหรับ Subscriptions ทดแทน WebSocket ในบาง Use cases
Improved React Server Components: Integration กับ Next.js App Router ดีขึ้น Server Component เรียก tRPC ได้โดยตรง
Better error handling: Error formatting ดีขึ้น รองรับ Zod error flatten
tRPC vs Hono RPC
| คุณสมบัติ | tRPC | Hono RPC |
|---|---|---|
| Type Safety | End-to-end | End-to-end |
| Runtime | Node.js, Edge | Node.js, Bun, Deno, Edge, Cloudflare Workers |
| Bundle Size | ปานกลาง | เล็กมาก (hono ~14KB) |
| React Query Integration | Built-in | ต้องทำเอง |
| Subscriptions | มี (WebSocket, SSE) | ต้องทำเอง |
| Community | ใหญ่กว่า | กำลังโต |
| Best For | Next.js Full-stack | Lightweight API, Edge computing |
Testing tRPC
// Unit test tRPC procedures
import { createCaller } from '../server/routers/_app';
import { createInnerContext } from '../server/context';
describe('user router', () => {
it('should get user by id', async () => {
const ctx = await createInnerContext({
session: { user: { id: 'test-user', role: 'USER' } },
});
const caller = createCaller(ctx);
const user = await caller.user.getById({ id: 'existing-id' });
expect(user).toHaveProperty('name');
expect(user).toHaveProperty('email');
});
it('should throw NOT_FOUND for invalid id', async () => {
const ctx = await createInnerContext({ session: null });
const caller = createCaller(ctx);
await expect(
caller.user.getById({ id: 'non-existent-id' })
).rejects.toThrow('NOT_FOUND');
});
it('should require auth for updateProfile', async () => {
const ctx = await createInnerContext({ session: null });
const caller = createCaller(ctx);
await expect(
caller.user.updateProfile({ name: 'Test' })
).rejects.toThrow('UNAUTHORIZED');
});
});
Deployment Considerations
Vercel: tRPC + Next.js deploy บน Vercel ได้ง่ายที่สุด API routes กลายเป็น Serverless functions อัตโนมัติ แต่ Subscriptions (WebSocket) ไม่รองรับบน Vercel
Cloudflare Workers: ใช้ fetchRequestHandler adapter ได้ แต่ต้องใช้ D1 หรือ Turso แทน Prisma ปกติ (เพราะ Workers ไม่รองรับ TCP connections)
Docker / VPS: Deploy ได้ปกติเหมือน Node.js app ทุกอย่าง รองรับ WebSocket subscriptions
สรุป — เมื่อไหร่ tRPC คือคำตอบ
tRPC เป็นทางเลือกที่ดีที่สุดเมื่อ คุณทำ Full-Stack TypeScript (Next.js, T3 Stack, Remix), ทั้ง Client และ Server อยู่ใน Monorepo, ไม่ต้องเปิด API สำหรับ Public consumption (mobile apps ภาษาอื่น), ต้องการ DX ที่ดีที่สุดและ Development speed สูงสุด
แต่ tRPC ไม่เหมาะเมื่อ คุณต้องเปิด Public API ให้ลูกค้าภายนอก (ใช้ REST + OpenAPI หรือ GraphQL), Client ไม่ได้เขียนด้วย TypeScript (ใช้ REST), มีทีม Backend กับ Frontend แยกกันที่ใช้ Schema เป็น Contract (ใช้ GraphQL)
สำหรับ Full-Stack TypeScript developer tRPC เปลี่ยนวิธีทำงานกับ API อย่างสิ้นเชิง ลดเวลาเขียน Code ลง 30-50% ลด Bug จาก Type mismatch เกือบ 100% และให้ Developer Experience ที่ดีที่สุดในตลาดวันนี้
