- tRPC เป็นเครื่องมือที่แชร์ การอนุมานของ TypeScript ในแอป TypeScript แบบ full-stack ช่วยลดความไม่ตรงกันของชนิดข้อมูล API ระหว่างเซิร์ฟเวอร์กับไคลเอนต์
- เมื่อแก้ API ฝั่งเซิร์ฟเวอร์ จะเห็นผลกระทบในโค้ดไคลเอนต์ได้ทันทีผ่าน ข้อผิดพลาดของ TypeScript และ autocomplete ทำให้ภาระในการ refactor ลดลง
- ทำงานได้โดยไม่ต้องมี schema แยกต่างหากหรือขั้นตอน code generation จึงสามารถสร้าง API แบบ type-safe ได้โดยไม่เพิ่มขั้นตอน build หรือภาระ runtime
- มี adapter สำหรับเชื่อมกับ สภาพแวดล้อม JavaScript หลายแบบ เช่น React, Next.js, Express, Fastify, AWS Lambda, Solid, Svelte
- โฟลว์พื้นฐานคือการนิยาม procedure, สร้าง HTTP server, และเชื่อมต่อไคลเอนต์โดยส่งชนิด
AppRouterโดยมุ่งให้ได้ประสบการณ์พัฒนาเหมือนใช้โค้ด API server เป็น SDK
tRPC เปลี่ยนวิธีพัฒนา API อย่างไร
- tRPC เพิ่มประสิทธิภาพการพัฒนาโดยทำให้เซิร์ฟเวอร์และไคลเอนต์ในแอปพลิเคชัน full-stack ใช้ ข้อมูลชนิดของ TypeScript ร่วมกัน
- แก่นหลักคือการสร้าง API แบบ end-to-end type-safe ได้ง่าย และลดงานซ้ำซ้อนในเลเยอร์ API แบบดั้งเดิม
- หากการเปลี่ยนแปลงฝั่งเซิร์ฟเวอร์ส่งผลต่อจุดที่ไคลเอนต์ใช้งาน TypeScript จะแจ้งข้อผิดพลาด จึงตรวจพบความไม่ตรงกันของชนิดข้อมูลที่ขอบเขตระหว่างไคลเอนต์กับเซิร์ฟเวอร์ได้รวดเร็ว
คุณสมบัติหลักที่นักพัฒนาสัมผัสได้
-
ความปลอดภัยของชนิดข้อมูลอัตโนมัติ
- เมื่อการเปลี่ยนแปลงฝั่งเซิร์ฟเวอร์ส่งผลต่อโค้ดไคลเอนต์ จะแสดงออกมาเป็นข้อผิดพลาดของ TypeScript
-
ประสบการณ์นักพัฒนาที่รวดเร็ว
- tRPC ไม่มีขั้นตอน build หรือ compile
- ใช้งานได้โดยไม่ต้อง code generation, ไม่เพิ่มภาระ runtime และไม่ต้องมีขั้นตอน build แยกต่างหาก
-
ไม่ผูกกับเฟรมเวิร์ก
- ใช้งานได้กับ JavaScript framework และ runtime โดยรวม
- เพิ่มเข้าในโปรเจกต์เดิมได้ง่าย
-
Autocomplete
- มอบประสบการณ์เหมือนจัดการโค้ด API server เป็น SDK
- รับคำแนะนำตามชนิดข้อมูลได้เมื่อใช้งาน endpoint
-
ขนาดฝั่งไคลเอนต์เล็ก
- tRPC ไม่มี dependency และมีขนาดฝั่งไคลเอนต์เล็ก
-
Adapter ที่มีมาให้
- มี adapter สำหรับ React, Next.js, Express, Fastify, AWS Lambda, Solid, Svelte และอื่น ๆ
โฟลว์การใช้งานพื้นฐาน
- API ของ tRPC เริ่มจากการนิยาม procedure ก่อน
- procedure คือฟังก์ชันที่ประกอบเป็น backend สามารถประกอบร่วมกันได้ และสร้างเป็น query, mutation, subscription ได้
- procedure หลายตัวจะถูกรวมไว้ใน router
-
การนิยาม procedure
- ในตัวอย่าง สร้างอินสแตนซ์ tRPC ด้วย
initTRPC.create()แล้วตั้งค่าrouterและpublicProcedure - procedure
greetingรับสตริงnameเป็น input และคืนค่าสตริงรูปแบบHello ${input.name} - การตรวจสอบ input ใช้ Zod เพื่อให้ input จากไคลเอนต์ตรงกับรูปแบบที่ procedure คาดหวังอย่างแม่นยำ
- ที่ท้ายไฟล์ ส่งออกชนิดของ router ด้วย
export type AppRouter = typeof appRouter;เพื่อให้ใช้ได้ในโค้ด frontend
- ในตัวอย่าง สร้างอินสแตนซ์ tRPC ด้วย
-
การสร้าง HTTP server
- ส่ง
appRouterให้createHTTPServerเพื่อรันเซิร์ฟเวอร์ tRPC - ในตัวอย่าง เรียก
listen(3000)เพื่อให้ API listen ที่พอร์ต3000 - tRPC มี adapter สำหรับ Next.js, Express, สภาพแวดล้อมที่ใช้ Fetch API, Fastify, AWS Lambda และ vanilla Node HTTP server
- ตัวอย่างสภาพแวดล้อมที่ใช้ Fetch API ได้แก่ Astro, Remix, SvelteKit, Cloudflare Workers
- ส่ง
-
การเชื่อมต่อไคลเอนต์และรัน query
- เมื่อเซิร์ฟเวอร์ทำงานแล้ว สามารถสร้างไคลเอนต์ด้วย
createTRPCClient<AppRouter>และ query ข้อมูลได้ - ไคลเอนต์ตัวอย่างใช้
httpBatchLinkเพื่อเชื่อมต่อกับhttp://localhost:3000 - เรียกใช้งานอย่าง
trpc.greeting.query({ name: 'John' })เพื่อใช้ proceduregreetingของเซิร์ฟเวอร์ได้ - เมื่อสร้างไคลเอนต์โดยส่งชนิด
AppRouterจะได้ TypeScript autocomplete และ IntelliSense ที่สอดคล้องกับ backend API โดยไม่ต้อง code generation
- เมื่อเซิร์ฟเวอร์ทำงานแล้ว สามารถสร้างไคลเอนต์ด้วย
เทมเพลตเริ่มต้นและเจตนาในการสร้าง
- มี Use this template เป็นเทมเพลตให้ทดลองได้ด้วยตนเอง
- ผู้สร้าง tRPC สร้าง tRPC ขึ้นเพื่อช่วยลดความจำเป็นของเลเยอร์ API แบบดั้งเดิม และให้ความมั่นใจว่าแอปจะไม่พังแม้จะพัฒนาแบบวนซ้ำอย่างรวดเร็ว
- tRPC ถูกใช้งานโดยทีมเทคโนโลยีชั้นนำและบริษัท Fortune 500 หลายแห่ง
กระแสตอบรับจากนักพัฒนา
- จากคำกล่าวอ้างอิงของนักพัฒนาหลายคน tRPC ถูกประเมินว่าเป็นเครื่องมือที่ช่วยปรับปรุงคุณภาพโค้ด ความเร็วในการส่งมอบ ความพึงพอใจของนักพัฒนา การ refactor ที่ขอบเขตไคลเอนต์·เซิร์ฟเวอร์ การตรวจสอบ input และประสบการณ์ middleware แบบมีชนิดข้อมูล
- ใน TypeScript monorepo ถูกมองว่าเป็นทางเลือกที่เรียบง่ายกว่า plain REST หรือ GraphQL แต่ยังให้ชนิดข้อมูลที่แข็งแรง
- มีการแนะนำกรณีใช้งานที่แม้จะคืนค่า payload ของ Stripe API จากเซิร์ฟเวอร์ ก็ยังรับชนิดข้อมูลของ response ใน React component ได้
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
ตอนนี้กำลังดำเนินการ เอา tRPC ออกจาก codebase อยู่ และมันเหมือนฝันร้ายเพราะการผูกกันแน่นมาก
มันมีแง่ที่ทำให้นักพัฒนารุ่นจูเนียร์ไม่ต้องคิดเรื่อง interface หรือ pattern การเข้าถึงข้อมูล และเกิด mapping ที่ไหลต่อจาก Prisma ไปถึง component ตรง ๆ
สำหรับการทำ prototype อย่างรวดเร็วนั้นยอดเยี่ยม แต่ถ้าภายหลังพยายามแยก codebase ออก ก็จะติดทางตันอย่างรวดเร็ว
เพราะมันขยายไปยังภาษาอื่นนอกเหนือจาก TypeScript ได้ด้วย จึงช่วยได้ตอนย้าย backend ไปใช้ภาษาอื่น หรือสร้าง native mobile client อย่าง Swift, Kotlin
tRPC ยอดเยี่ยม แต่ท้ายที่สุดมันก็เป็นเพียง transport layer ระหว่าง backend กับ frontend เท่านั้น
การปล่อยให้ใช้โครงสร้างภายในของ tRPC ลึกเข้าไปใน business logic นั้นแย่พอ ๆ กับการไม่มีชั้น controller หรือ router ที่นิยาม input, schema และการแยกส่วนอย่างชัดเจน
ดังนั้นถ้าภายหลังจะเลิกใช้ tRPC ก็น่าจะค่อนข้างง่าย และเช่น การย้ายทั้ง subsystem ให้ไปรันบน queue ก็น่าจะไม่ยากนัก
เรื่องแบบนี้มี learning curve และโดยปกติจะเริ่มจาก type ที่ไร้ประโยชน์และไม่ได้อธิบายอะไรเลยอย่าง
type FunctionIWroteTodayArgs = …หลังทำซ้ำไปไม่กี่รอบ ก็จะค่อย ๆ ตระหนักว่าเป้าหมายไม่ใช่การทำซ้ำโค้ด แต่คือการ อธิบาย domain และสร้าง type, interface, API ที่นำกลับมาใช้ซ้ำได้และมีประโยชน์
ดังนั้นผมคิดว่าควรปรับปรุงส่วนนั้นร่วมกับทีม มากกว่าจะถอน tRPC ออก
ยังไม่ค่อยเข้าใจว่าการประกาศ HTTP client ฝั่ง server แล้วให้ฝั่ง client consume มันแย่กว่าตรงไหน
เราสร้าง service และใช้ pattern ที่ผู้บริโภคทั้งหมดอย่าง web interface หรือ CLI ใช้ร่วมกัน และการที่สิ่งเหล่านี้ไม่พังถือเป็นการปรับปรุงครั้งใหญ่กว่าวิธีใด ๆ ที่เคยเห็นมาก่อน
ข้อบ่นว่านักพัฒนารุ่นจูเนียร์ไม่คิดเรื่อง interface น่าจะเกิดขึ้นได้เหมือนกันไม่ว่าจะใช้ API แบบใด ไม่ใช่แค่ tRPC
ที่ Notion เคยใช้สไตล์ API คล้าย tRPC และ API นั้นมีมาก่อน tRPC ราว 4 ปี
เรื่องแบบนี้สร้างเองได้ง่ายด้วย mapped types ของ TypeScript แค่สร้าง object type ที่ key คือชื่อ API และ value คือ type แบบ
{ request, response }ฝั่ง server กำหนดแต่ละ API handler เป็นฟังก์ชันที่รับ
APIs["addUser"]["request"]แล้วคืนค่าPromiseส่วน client ก็ expose เป็น async function ที่มี argument และ return type เดียวกันใช้กลยุทธ์นี้กับ HTTPS internal API, real-time API บน WebSocket, IPC ระหว่าง Electron กับ Webview และ OS webview IPC ระหว่าง native iOS/Android กับ Webview
ใน native API ฝั่ง server เป็น Swift หรือ Kotlin จึงเขียน request/response type ซ้ำด้วยมือใน TypeScript สักวันหนึ่งคงเปลี่ยนไปใช้ binary format ที่มี IDL ของตัวเอง แต่สำหรับ API ข้ามภาษาแบบเดี่ยวที่ค่อย ๆ โตขึ้น ต้นทุนด้าน developer experience ของสิ่งอย่าง Protobuf ยังดูไม่คุ้มค่า
มันไม่สมบูรณ์แบบ แต่ให้ API interface ที่ค่อนข้างดีพร้อม type ของ request/response
นอกจากนี้ยังทำ mock API wrapper ขนาด 10 บรรทัดที่เรียกได้แบบ
mockApi((request) => response)โดย type-check ว่า mock function implement API ถูกต้อง และคืนฟังก์ชันที่หน้าตาเหมือนฟังก์ชัน API จริงทุกประการ[0]: https://github.com/ferdikoomen/openapi-typescript-codegen
ดูเหมือนว่ายังไม่มีไลบรารีที่ดูแลดีและโปรโมตดีซึ่งใช้แนวทางนี้ออกมา จึงไม่ค่อยถูกพูดถึงบ่อยนัก
คุณค่าที่สำคัญเวลาใช้ tRPC, JSON Schema พร้อมการสร้าง type, หรือ Zod ก็คือส่วนนั้นเอง
ถึงขั้นให้ส่ง generic argument เองตอนเรียก ซึ่งค่อนข้างไม่ดีนักเมื่อพยายามรักษา dependency tree ให้เป็นระเบียบ
https://github.com/mikew/transmission-material-ui/blob/maste...
https://github.com/mikew/transmission-material-ui/blob/maste...
ชอบ tRPC มาก น่าทึ่งตรงที่มันผลักดัน ประสบการณ์นักพัฒนา ไปจนสุดทางในสแต็กที่ใช้ TypeScript ล้วน และทำให้ชุมชน GraphQL ตระหนักถึงข้อจำกัดและการประนีประนอมของภาษาคิวรี
ในขณะเดียวกัน tRPC ก็ผ่านวงจรความนิยมไปอย่างรวดเร็วมาก และดูเหมือนจะไม่ได้เกิดการย้ายครั้งใหญ่จาก REST และ GraphQL ไปเป็น RPC
แต่ช่วงนี้ดูเหมือนผู้คนสนใจ RPC กันมาก และในเฟรมเวิร์ก BFF ที่เราสร้างขึ้น (https://wundergraph.com/) เราก็นำแนวคิดบางส่วนจาก tRPC และ NextJS รุ่นเก่ามาใช้ โดยผสาน file-based routing เข้ากับ RPC
นอกจาก tRPC แล้ว เรายังสร้าง JSON Schema อัตโนมัติให้แต่ละงาน และสร้างสเปก OpenAPI สำหรับชุดงานทั้งหมดด้วย
ผู้คนชอบแนวทางนี้ เพราะสามารถแชร์ชุด RPC endpoint เป็นสเปก OpenAPI หรือ Postman collection ได้ง่าย แทบไม่มีการถกเรื่อง HTTP verb และในทางปฏิบัติก็มีแค่ query, mutation, subscription
อยากรู้ว่าช่วงนี้แต่ละคนใช้ API สไตล์ GraphQL, REST, RPC กันอย่างไร และมีคนหรือทีมกี่ทีมที่เกี่ยวข้องกับ API
ฝั่ง REST API มี ts-rest(https://ts-rest.com), zodios(https://www.zodios.org), Hono(https://hono.dev)
ถ้าทีมใช้หลายภาษา ก็มี Fern ด้วย: https://www.buildwithfern.com
ทุกครั้งที่ federated schema เปลี่ยน เราจะสร้าง type ของ GraphQL และทุกครั้งที่บันทึกไฟล์ เราจะสร้าง type ของ response สำหรับ query และ mutation
มันยังสามารถถูก abstract ได้ง่ายด้วยไลบรารีสำหรับสร้าง endpoint consumer แต่ไม่รู้ว่านอกจากนั้นแล้วได้อะไรเพิ่มจริง ๆ เมื่อเทียบกับเว็บ API ทั่วไป
ยังไงงานที่ต้องทำในแต่ละ backend call ก็ยังต้องทำเหมือนเดิมอยู่ดี
ชอบ tRPC เท่าที่เคยเห็นมา นี่คือประสบการณ์การพัฒนาแบบ full-stack ที่ดีที่สุดแบบห่างชั้น โดยเฉพาะเมื่อใช้คู่กับ Zod แล้ว API จะยอดเยี่ยมมาก
ผมมองว่า Zod และ tRPC เป็นโปรเจกต์สำคัญในอนาคตของ TypeScript และคิดว่าในอีกไม่กี่ปีข้างหน้า ประสบการณ์นักพัฒนาที่ได้รับแรงบันดาลใจจาก tRPC จะเบ่งบานอย่างมากทั่วทั้งระบบนิเวศ TypeScript
โปรเจกต์ที่มี DNA ของ tRPC ชัดเจนแล้วก็มี UploadThing ของ Ping (https://github.com/pingdotgg/uploadthing) ซึ่งมุ่งกรณีใช้งานอื่น และ Lusat ของเรา (https://github.com/lusatai/lusat)
อีกอย่างคือ type ของ validation error เปลี่ยนไปตาม schema ที่ตรวจ ซึ่งก็น่ารำคาญ การจัดการ error กลายเป็นคาดเดายาก และมีกรณียกเว้นมากเกินไปเสมอ
ยังมีพื้นที่ให้ปรับปรุงอีกมาก
การเขียน unit test ก็ง่ายขึ้นมากด้วย
ปัญหาใหญ่ดูเหมือนจะเป็น CRUD ที่ type-safe กับ data migration แม้จะมีคนพูดถึง Mongoose กันมาก แต่ในแง่ type safety รู้สึกเหมือนถอยหลังไปมากเมื่อเทียบกับ Zod/TypeScript
ตอนนี้อยากพูดแบบเว่อร์นิดหน่อยว่า ในสไตล์การเขียนโปรแกรมบางแบบ การเพิ่ม Zod เข้าไปใน codebase ให้ประโยชน์มากพอ ๆ กับการเพิ่ม TypeScript เลย
แพตเทิร์นที่เริ่มจาก schema แล้วสร้าง service ที่มี type เข้ากับคนที่เอนเอียงไปทาง functional programming ได้ดี
สงสัยว่า tRPC จัดการ ความต่างของเวอร์ชัน และ migration อย่างไร
field มีวงจรชีวิตของมัน ตอนเริ่มนำเข้ามา ยังไม่มี client หรือ server ใดรู้จัก field นั้น
client และ server ไม่ได้ถูก restart ทั้งหมดพร้อมกัน และไม่ได้มี type เวอร์ชันเดียวกันด้วย
ข้อมูลจะถูกเขียนด้วย type เวอร์ชันหนึ่ง และถูกอ่านด้วย type อีกเวอร์ชันหนึ่ง
ถ้ารับประกันได้ว่า binary ทุกตัว โปรแกรมที่กำลังรันทุกตัว และข้อมูลทั้งหมดจะถูกอัปเกรด และไม่มี persistent data ก็อาจไม่ใช่ปัญหา
แต่เมื่อมีหลายองค์กรเข้ามาเกี่ยวข้อง ก็ยากที่จะรับประกันว่า app และ server ทุกตัวจะ sync กับ library เวอร์ชันล่าสุด ถูก build ใหม่ และ deploy ใหม่
static type checking สมมติว่าไม่มีความต่างของเวอร์ชัน โค้ดทั้งหมดใน binary เป็นโลกปิดที่ build ด้วย library เวอร์ชันเดียวกันที่นิยาม type
Zod สามารถอนุญาต field ที่ไม่รู้จักได้ จากนั้นค่อยย้ายไปเป็น optional field และเมื่อสถานการณ์นิ่งแล้วค่อยทำให้เป็น required field
แน่นอนว่ายิ่งเป็นกลุ่มที่อยากได้สิ่งที่ tRPC ทำให้มากที่สุด ก็ยิ่งมีแนวโน้มจะคิดถึงปัญหาแบบนี้น้อยที่สุด
ถ้ามุ่งเป้าแค่ภาษาเดียว ก็เป็นเรื่องธรรมดาที่ไม่ต้องมี schema หรือ code generation
ใช้ tRPC กับเว็บแอปพลิเคชันสองตัวที่มีโค้ดราว 50,000 บรรทัด และชอบมาก ประสบการณ์นักพัฒนา ยอดเยี่ยม
แต่ช่วงความนิยมของ tRPC ผ่านไปได้สักพักแล้ว และรู้สึกว่ากระแส RSC ตามมาทันอย่างรวดเร็ว
เดี๋ยวนี้ทั้งที่มีทางออกที่เสถียรและดีอย่าง tRPC อยู่แล้ว ทุกคนกลับคุยกันแต่เรื่องว่าจะใช้ RSC หรือไม่
ไม่ได้ต่อต้าน RSC แต่มีการถกกันมากเกินไป เวลาสร้างแอปพลิเคชันในตอนนี้ tRPC เป็นแนวทางที่ใช้งานได้จริงมาก
เพิ่มเติม: RSC คือ React Server Components และมาพร้อมปรัชญาเรื่องข้อมูลแบบอ่าน/เขียนของตัวเอง
ตัวย่อที่คนอาจไม่รู้ ควรนิยามหรือใส่ลิงก์ไว้ด้วย
เคยลองใช้ tRPC กับ Next.js ในโปรเจกต์ส่วนตัวอยู่บ้าง และเป็นประสบการณ์ที่ดี
โดยเฉพาะถ้าใช้ร่วมกับเทมเพลตที่ตั้งค่าไว้ล่วงหน้าอย่าง Create T3 App(https://create.t3.gg/) ความเร็วในการวนรอบพัฒนานั้นยากจะเอาชนะได้
ก่อนที่นักพัฒนาที่เคยทำงานด้วยกันจะยกย่อง tRPC อย่างมากก็ไม่รู้จักมาก่อน แต่พอได้รู้แล้วก็ดูเหมือนว่ามันดีอย่างชัดเจนมาก
เราสร้างแอป T3 (tRPC, Next.js, Tailwind, TypeScript, Prisma) ด้วยกัน และถ้าอยากดู อยู่ที่นี่: https://github.com/stytchauth/stytch-t3-example
เมื่อทำงานด้วย TypeScript API ที่มี type safety ช่วยได้มากจริง ๆ