Devfavor

Back

tRPC คืออะไร — เรียก API เหมือนเรียกฟังก์ชันธรรมดาBlur image

เคยเจอไหม — แก้ชื่อ field ในฝั่ง backend จาก userName เป็น name แล้วลืมไปแก้ฝั่ง frontend พอรันถึงรู้ว่าพัง เพราะ frontend ยังเรียก user.userName อยู่ ไม่มีอะไรเตือนเลยจนกว่าจะเปิดหน้าเว็บแล้วเห็น undefined

ปัญหานี้เกิดเพราะ frontend กับ backend ไม่ได้รู้จักกัน frontend ยิง HTTP ไปที่ URL แล้วได้ JSON ก้อนหนึ่งกลับมา ส่วนหน้าตาของ JSON ก้อนนั้นเป็นยังไง frontend ต้องเดาเอาเองหรือไปเปิด docs ดู ทุกครั้งที่ backend เปลี่ยน ฝั่ง frontend จะรู้ก็ต่อเมื่อโค้ดพังไปแล้ว

ทางแก้แบบเดิมคือเขียน type ขึ้นมาเองทั้งสองฝั่ง หรือไม่ก็รัน code generation จาก schema ทุกครั้งที่แก้ ซึ่งก็ทำงานได้ แต่มีขั้นตอนเพิ่ม มีโอกาสลืมรัน และมี boilerplate เต็มไปหมด

tRPC เกิดมาเพื่อตัดปัญหานี้ออกไป — ถ้าทั้ง frontend และ backend เป็น TypeScript เหมือนกันอยู่แล้ว ทำไมต้องมา sync type กันเองด้วย ในเมื่อ TypeScript รู้อยู่แล้วว่า function ฝั่ง server รับอะไรและคืนอะไร


🤔 tRPC คืออะไร#

tRPC ย่อมาจาก TypeScript Remote Procedure Call เป็นไลบรารีสำหรับสร้างและเรียกใช้ API ที่ type-safe ตั้งแต่ต้นจนจบ (end-to-end typesafe) โดยไม่ต้องมี schema แยก และไม่ต้องมีขั้นตอน code generation

หัวใจอยู่ที่คำว่า RPC (Remote Procedure Call) — แทนที่จะยิง HTTP request ไปที่ URL แบบ REST ก็เรียกฟังก์ชันบนเครื่อง server โดยตรงเหมือนเรียกฟังก์ชันปกติ

// REST: ยิงไปที่ URL แล้วเดาว่าได้อะไรกลับมา
const res = await fetch('/api/users/1');
const user = await res.json(); // user เป็น any — TypeScript ไม่รู้อะไรเลย

// tRPC: เรียกเหมือนฟังก์ชัน แล้ว type มาครบ
const user = await trpc.userById.query('1'); // user มี type ชัดเจน
ts

เคล็ดลับคือ tRPC ไม่ได้ใช้ code generation แต่ใช้ความสามารถ type inference ของ TypeScript โดยตรง ฝั่ง client แค่ import type ตัว router มาจาก backend เท่านั้น TypeScript ก็รู้ทันทีว่าแต่ละ procedure รับ input อะไรและ return อะไร พูดง่าย ๆ คือ โค้ด TypeScript เองคือ schema ไม่ต้องมี schema language แยกอีกภาษา

ที่สำคัญคือ type พวกนี้เป็นเรื่องของ development-time เท่านั้น — ไม่มี type อะไรวิ่งข้าม network ตอน runtime เบื้องหลังก็ยังเป็น HTTP ธรรมดา แค่หน้าตาเวลาเขียนเปลี่ยนไป


🧩 ส่วนประกอบหลัก#

โครงสร้างของ tRPC มีไม่กี่ชิ้น แต่ประกอบกันได้ยืดหยุ่น

  • Procedures — หน่วยพื้นฐานของ backend หนึ่ง procedure คือหนึ่ง endpoint แบ่งเป็น query (อ่านข้อมูล), mutation (เขียน/แก้ข้อมูล) และ subscription (รับข้อมูลแบบ real-time)
  • Routers — กลุ่มของ procedures ที่อยู่ภายใต้ namespace เดียวกัน เช่นรวม procedure ทั้งหมดที่เกี่ยวกับ user ไว้ด้วยกัน
  • Context — ข้อมูลที่ทุก procedure เข้าถึงได้ เช่น session, database connection
  • Middleware — ตัวคั่นก่อนเข้า procedure เช่นเช็คว่า login หรือยัง
  • Validation — ตรวจ input ก่อนเข้า logic นิยมใช้คู่กับ Zod

ฝั่ง server หน้าตาประมาณนี้

export const appRouter = router({
  userById: publicProcedure
    .input(z.string())
    .query(async (opts) => {
      return { id: opts.input, name: 'Katt' };
    }),
  userCreate: publicProcedure
    .input(z.object({ name: z.string() }))
    .mutation(async (opts) => {
      return { id: '1', ...opts.input };
    }),
});

export type AppRouter = typeof appRouter;
ts

จุดที่ทำให้ทุกอย่างเชื่อมกันคือบรรทัดสุดท้าย — export type AppRouter ฝั่ง client เอา type ตัวนี้ไปใส่เป็น generic แล้วก็ได้ type ของ procedure ทุกตัวมาฟรี ทั้ง autocomplete และ error ตอน build time

const trpc = createTRPCClient<AppRouter>({
  links: [httpBatchLink({ url: 'http://localhost:3000' })],
});

await trpc.userById.query('1');
await trpc.userCreate.mutate({ name: 'Katt' });
ts

ถ้าวันหนึ่ง backend แก้ให้ userCreate ต้องรับ email เพิ่ม ฝั่ง client จะขึ้น type error ทันทีตั้งแต่ตอนเขียน ไม่ต้องรอให้พังตอนรัน


✅ ข้อดี#

จุดขายหลักของ tRPC คือ developer experience ที่ดีมากเมื่อทำงานเต็ม stack ด้วย TypeScript

แก้ return type ฝั่ง backend ปุ๊บ frontend throw error ปั๊บ — ได้ feedback ทันทีตอน refactor ไม่ต้องไล่หาเองว่าตรงไหนยังเรียกของเก่าอยู่ และเพราะ type derive ตรงจากโค้ด จึง sync กันเสมอ ไม่ต้องรัน codegen หลังแก้ schema เหมือน GraphQL

อีกข้อคือ bundle เล็ก ฝั่ง client ของ tRPC อยู่ที่ราว 15KB minified เทียบกับ Apollo Client ที่ราว 130KB เพราะ tRPC ไม่มี query parser หรือ schema runtime ฝั่ง client

นอกจากนั้นยังตัด boilerplate ของ REST/GraphQL ออกเยอะ — ไม่ต้องเขียน route handler, controller, serializer หรือ maintain OpenAPI/Swagger spec แยก เขียน function แล้วก็ใช้ได้เลย

stack ที่นิยมเรียกว่า T3 Stack คือ TypeScript + Next.js + tRPC + Tailwind CSS + Prisma โดยมักเสริมด้วย NextAuth, Zod และ React Query ซึ่งเหมาะกับงาน full-stack TypeScript ภายในอย่าง dashboard, SaaS หรือ internal tool ที่ต้องการความเร็วในการพัฒนาสูง ๆ

และนี่ไม่ใช่แค่ของเล่น — มีโปรเจกต์ open-source ขนาดใหญ่และบริษัทหลายแห่งที่ใช้ T3 Stack ใน production จริง


⚠️ ข้อเสียและข้อจำกัด#

ข้อจำกัดที่ใหญ่ที่สุดของ tRPC คือ ผูกกับ TypeScript ทั้งสองฝั่ง — client และ server ต้องเป็น TypeScript ในโค้ดเบสเดียวกัน และทีมต้อง control ทั้งคู่

ทันทีที่มี client ภาษาอื่นเข้ามา tRPC จะใช้ไม่ได้ ถ้าจะทำ iOS app ด้วย Swift หรือมี service Python มาเรียก API คุณจะต้องสร้าง type ขึ้นมาเองในภาษานั้น ซึ่งทำลายจุดประสงค์ทั้งหมดของ tRPC ไปเลย กรณีพวกนี้ต้องถอยไปใช้ REST หรือ GraphQL แทน

เพราะแบบนี้ tRPC จึง ไม่เหมาะกับ public API ที่ต้องเปิดให้คนนอกเรียก หรือ backend ที่มีหลายภาษา (polyglot)

อีกข้อที่หลายคนไม่ทันคิดคือ ไม่มี self-documenting API แบบ GraphQL หรือ OpenAPI เพราะ API contract ทั้งหมดอยู่ใน TypeScript types ไม่มีหน้า docs ที่ generate ออกมาให้คนนอกอ่าน


🐛 Gotcha ที่เจอบ่อย#

มีอยู่สามเรื่องที่มักทำให้คนเริ่มใช้ tRPC แล้วสะดุด

หนึ่ง — TypeScript ช้าลงเมื่อ router ใหญ่ขึ้น เมื่อ router มี procedures เยอะ (ราวเกิน 100 ตัว) IntelliSense อาจหน่วงถึงหลายวินาทีต่อการแก้ไฟล์ เพราะการ import type ของ router ทำให้ tsc ต้องดูดและเช็ค backend ทั้งหมดเข้ามา ปัญหาจะหนักขึ้นเมื่อใช้คู่กับ Zod และ Drizzle ที่มี nested type ลึก ๆ ทำให้ compiler ต้อง resolve ทั้งหมดทุกครั้ง วิธีบรรเทาคือแยก router ตาม domain, split โปรเจกต์เป็น API / database / shared types และ precompile ส่วนที่ build นาน

สอง — N+1 query problem เกิดใน resolver ที่ loop ผ่าน list แล้วทำ DB lookup ทีละ item เรื่องนี้ไม่ใช่ปัญหาเฉพาะ tRPC แต่เจอบ่อยเวลาเขียน resolver แบบไม่ระวัง

สาม — ลืม validate input ถ้าข้าม validation ไป จะเปิดช่องให้เกิดทั้ง bug และช่องโหว่ด้าน security เพราะฉะนั้น .input() กับ Zod ควรเป็นนิสัย ไม่ใช่ของเสริม


🧭 เมื่อไหร่ควรใช้ เมื่อไหร่ไม่ควร#

ควรใช้ เมื่อทีมเดียวเป็นเจ้าของทั้ง frontend และ backend, ทั้งสองฝั่งเป็น TypeScript/Node, อยู่ใน monorepo เดียวกัน และต้องการ type safety บวกความเร็วในการพัฒนาสูงสุด งานแบบ full-stack TypeScript บน Next.js คือจุดที่ tRPC เปล่งประกายที่สุด

(tRPC v11 ยังเพิ่ม native support สำหรับ React Server Components ด้วย และใน Next.js App Router ยังเรียก procedure ตรงจาก Server Component ได้ผ่าน createCaller แบบไม่ต้องยิง HTTP เลย)

ไม่ควรใช้ เมื่อมี client หลายภาษาหรือหลายแพลตฟอร์ม, ต้องเปิด API ให้คนนอก, หรือมี service ภาษาอื่นมาเรียก กรณีพวกนี้ REST หรือ GraphQL เหมาะกว่า

ถ้าชอบแนวทาง type-safe แบบ tRPC แต่ติดเรื่อง cross-language ลองดู oRPC ที่ยืมจุดเด่นของ tRPC มาแล้วเพิ่ม first-class OpenAPI generation ทำให้ใช้ข้ามภาษาได้ หรือ ts-rest เป็นอีกตัวเลือก (แต่ยังขาดบางฟีเจอร์อย่าง flexible middleware และการ handle Dates/Files)

ในความเป็นจริง คุณไม่จำเป็นต้องเลือกแค่ตัวเดียว — pattern ที่นิยมคือผสมกัน: ใช้ REST สำหรับ public API ภายนอก, tRPC หรือ GraphQL สำหรับ internal frontend และ gRPC หรือ REST สำหรับ service-to-service สำหรับงาน browser-to-server ทั่วไป performance ของ REST/GraphQL/tRPC แทบไม่ต่างกันเพราะ network latency เป็นตัวครอบงำอยู่แล้ว เลือกตามความเหมาะกับทีมและ stack ได้เลย


สรุปสั้น ๆ tRPC คือคำตอบสำหรับคำถามที่ว่า “ถ้าทั้ง frontend และ backend เป็น TypeScript อยู่แล้ว ทำไมต้องมา sync type กันเองด้วย” — เขียน function ฝั่ง server แล้วฝั่ง client ได้ type มาครบ ไม่มี schema ไม่มี codegen ไม่มี boilerplate

ถ้าทีมคุณเข้าเงื่อนไข (TypeScript ทั้ง stack, monorepo, ทีมเดียวคุมทั้งคู่) tRPC จะให้ทั้งความเร็วและความมั่นใจที่หาจากที่อื่นยาก แต่ถ้าหลุดจากกรอบนั้นเมื่อไหร่ — มี client ภาษาอื่น หรือต้องเปิด API ให้คนนอก — ก็อย่าฝืน เลือก REST หรือ GraphQL จะสบายใจกว่า

อยากลองจริง แนะนำให้เริ่มจาก quickstart ของ official docs ที่ trpc.io ↗ แล้วลองสร้าง router เล็ก ๆ ดูสัก procedure สองอันก่อน จะเห็น autocomplete วิ่งข้ามจาก backend มา frontend ด้วยตาตัวเองว่า tRPC ต่างจากที่เคยเขียนยังไง

😎 ☕

Code, coffee, and calm — the holy trinity of a good day. ☕

tRPC คืออะไร — เรียก API เหมือนเรียกฟังก์ชันธรรมดา
Author Coffee Stack
Published at 25 มิถุนายน 2026