Tăng tốc Phát triển Full-Stack với TypeScript: Tại sao tRPC là Người bạn đồng hành mới của bạn

Nếu bạn đã từng xây dựng các ứng dụng web full-stack, có lẽ bạn quen thuộc với quy trình lặp đi lặp lại: thiết lập backend (thường với Node.js và Express), tạo các endpoint REST API chuẩn, và sau đó viết code ở frontend (như React) để gọi các API đó.

Quy trình này hoạt động, nhưng nó thường bao gồm các bước lặp đi lặp lại tốn thời gian:

  • Xác định tuyến (route) API trên máy chủ (backend).
  • Xác định kiểu dữ liệu (data types) cho dữ liệu đầu vào và đầu ra của tuyến API đó.
  • Viết code ở frontend để gọi endpoint cụ thể đó.
  • Xác định lại kiểu dữ liệu ở frontend để đảm bảo dữ liệu nhận được khớp với mong đợi.
  • Thường cần thêm các thư viện quản lý trạng thái phức tạp như Redux chỉ để xử lý việc gọi API, hiển thị trạng thái loading, và xử lý lỗi ở frontend.

Điều này dẫn đến việc thông tin về API của bạn (đường dẫn, kiểu dữ liệu) bị lặp ở hai nơi: backend và frontend. Bất kỳ thay đổi nào ở backend đều yêu cầu cập nhật tương ứng ở frontend, tiềm ẩn nguy cơ xảy ra lỗi nếu bạn quên đồng bộ hóa. Đối với các nhà phát triển cá nhân hoặc nhóm nhỏ muốn di chuyển nhanh, việc quản lý sự đồng bộ này có thể rất chậm và khó chịu.

Trong hành trình tìm kiếm một cách hiệu quả hơn để xây dựng ứng dụng nhanh chóng, đảm bảo tính chính xác và giữ cho code sạch sẽ, việc tìm ra một giải pháp cho vấn đề này trở nên cực kỳ quan trọng.

Khám phá tRPC: Một Cách Tiếp Cận Đơn Giản Hơn

Năm 2023, một thư viện TypeScript tên là tRPC bắt đầu thu hút sự chú ý. Ý tưởng cốt lõi của nó rất hấp dẫn: điều gì sẽ xảy ra nếu code frontend của bạn có thể gọi code backend trực tiếp, giống như gọi một hàm thông thường, và tất cả các kiểu dữ liệu sẽ tự động khớp nhau?

Ban đầu có thể nghe như một lời quảng cáo, nhưng sau khi dành thời gian thử nghiệm trên một dự án nhỏ, kết quả thật đáng kinh ngạc. tRPC đã làm cho quá trình phát triển trở nên đơn giản và nhanh hơn rất nhiều.

tRPC Hoạt động như thế nào? Tổng quan nhanh

Với tRPC, bạn không cần phải suy nghĩ theo cách truyền thống về các endpoint REST. Thay vào đó, bạn định nghĩa các “procedures” (thủ tục) ở backend. Hãy coi chúng như những hàm mà frontend của bạn có thể gọi. Bạn có thể sử dụng các thư viện hỗ trợ mạnh mẽ như Zod để định nghĩa rõ ràng kiểu dữ liệu đầu vào và đầu ra cho các procedures này.

Đây là một cái nhìn cơ bản về cách nó hoạt động:

Code Backend (Server Code):

// server.ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod'; // Zod giúp định nghĩa kiểu dữ liệu và kiểm tra validation

// Khởi tạo tRPC với cấu hình mặc định
const t = initTRPC.create();

// Định nghĩa router chính chứa các procedure mà frontend có thể gọi
export const appRouter = t.router({
  // Một procedure để lấy dữ liệu người dùng dựa trên ID
  getUser: t.procedure
    .input(z.object({ userId: z.string() })) // Yêu cầu đầu vào là một object có thuộc tính userId kiểu string
    .query(({ input }) => {
      // Code thực tế để tìm người dùng trong database...
      // Ví dụ trả về dữ liệu giả
      return { id: input.userId, name: 'Người dùng Ví dụ' }; // Trả về một object người dùng
    }),

  // Một procedure để tạo người dùng mới
  createUser: t.procedure
    .input(z.object({ name: z.string() })) // Yêu cầu đầu vào là một object có thuộc tính name kiểu string
    .mutation(({ input }) => {
      // Code thực tế để tạo người dùng trong database...
      // Ví dụ trả về dữ liệu người dùng mới được tạo
      return { id: 'newUser123', name: input.name }; // Trả về object của người dùng mới
    }),
});

// Xuất kiểu dữ liệu của router của chúng ta. Đây là chìa khóa!
export type AppRouter = typeof appRouter;

Trong ví dụ trên, chúng ta đã tạo hai procedures: getUser (một query, thường dùng cho các hoạt động đọc dữ liệu) và createUser (một mutation, thường dùng cho các hoạt động ghi/sửa dữ liệu). Zod giúp kiểm tra dữ liệu đầu vào. Phần quan trọng nhất là việc xuất kiểu dữ liệu AppRouter – điều này cho TypeScript biết chính xác API của chúng ta trông như thế nào.

Code Frontend (Client Code sử dụng React):

tRPC tích hợp rất tốt với các thư viện quản lý dữ liệu frontend như React Query. Bạn thiết lập một client tRPC ở frontend và cung cấp cho nó kiểu AppRouter mà bạn đã xuất từ backend.

// client.tsx (Ví dụ sử dụng React)
import { createTRPCReact } from '@trpc/react-query';
// Nhập kiểu dữ liệu trực tiếp từ backend! Đây là điều kỳ diệu!
import type { AppRouter } from './server';

// Tạo client tRPC React, liên kết với kiểu AppRouter từ backend
const trpc = createTRPCReact<AppRouter>();

function UserProfile() {
  // Hook useQuery của tRPC biết chính xác procedure 'getUser' cần gì và trả về gì nhờ kiểu AppRouter!
  const userQuery = trpc.getUser.useQuery({ userId: '123' });

  if (userQuery.isLoading) return <div>Đang tải...</div>;
  if (userQuery.error) return <div>Lỗi: {userQuery.error.message}</div>;

  // Nhờ tRPC và TypeScript, chúng ta biết userQuery.data chắc chắn có thuộc tính 'name'
  return <div>Tên người dùng: {userQuery.data.name}</div>;
}

function CreateUserButton() {
  // Hook useMutation của tRPC cho procedure 'createUser'
  const mutation = trpc.createUser.useMutation();

  const handleCreate = () => {
    // tRPC kiểm tra xem chúng ta có gửi đúng kiểu dữ liệu đầu vào không ({ name: string })
    mutation.mutate({ name: 'Người dùng Mới' });
  };

  return <button onClick={handleCreate} disabled={mutation.isLoading}>Tạo người dùng</button>;
}

Bạn có thấy cách frontend nhập kiểu dữ liệu AppRouter không? Nhờ đó, hook trpc.getUser.useQuery tự động biết nó cần một thuộc tính userId trong đối số đầu vào và dữ liệu trả về (userQuery.data) sẽ có các thuộc tính idname với kiểu dữ liệu chính xác. Bạn không còn phải viết các hàm fetch API thủ công hay định nghĩa lại kiểu dữ liệu cho API ở phía client nữa. TypeScript chỉ đơn giản là “biết”!

Những Lợi ích Thực sự mà tRPC mang lại

Việc áp dụng tRPC đã giải quyết được những vấn đề nhức nhối nhất trong quá trình phát triển full-stack:

  1. Tốc độ Phát triển Nhanh hơn: Không còn lặp lại thông tin API hai lần. Khi một hàm/procedure được định nghĩa ở backend, nó ngay lập tức sẵn sàng sử dụng với kiểu dữ liệu chính xác ở frontend. Điều này giúp xây dựng các tính năng mới nhanh hơn đáng kể.
  2. Ít Lỗi hơn: Vì kiểu dữ liệu được chia sẻ và đồng bộ giữa backend và frontend, TypeScript sẽ ngay lập tức báo cho bạn biết nếu có sự không khớp. Nếu bạn thay đổi một procedure ở backend (ví dụ: thêm hoặc xóa một thuộc tính trong dữ liệu trả về), trình soạn thảo code của bạn sẽ báo lỗi ở frontend ngay lập tức, trước khi bạn chạy ứng dụng. Điều này ngăn chặn một lượng lớn lỗi runtime không đáng có.
  3. Trải nghiệm Lập trình viên Tuyệt vời hơn (DX): Trình soạn thảo code có thể tự động hoàn thành (autocomplete) các lệnh gọi API ở frontend một cách chính xác. Hơn nữa, các thư viện như React Query xử lý tất cả các phần phức tạp của việc tìm nạp dữ liệu (như caching, trạng thái loading, cập nhật dữ liệu) một cách dễ dàng nhờ sự tích hợp chặt chẽ với tRPC. Bạn không cần dựa vào các thư viện quản lý trạng thái phức tạp chỉ để xử lý dữ liệu từ server nữa.

tRPC trong Thực tế: Xây dựng Ứng dụng Quy mô

tRPC không chỉ dừng lại ở các dự án nhỏ. Nó đủ mạnh mẽ để được sử dụng làm xương sống cho toàn bộ backend của các ứng dụng thực tế, kể cả các ứng dụng cần mở rộng. Việc định nghĩa các procedures ở backend mang lại cho frontend những hàm type-safe, dễ sử dụng một cách tức thì, giúp quá trình phát triển và bảo trì trở nên hiệu quả hơn rất nhiều.

Khi nào tRPC có thể không phải là Lựa chọn Phù hợp?

tRPC là một lựa chọn xuất sắc khi bạn kiểm soát cả frontend và backend, và đặc biệt là khi cả hai đều sử dụng TypeScript (hoặc JavaScript với kiểm tra kiểu tĩnh). Nó tạo ra một sự kết nối chặt chẽ và liền mạch giữa hai lớp này.

Tuy nhiên, nếu bạn đang xây dựng một API để người khác sử dụng (một API công cộng), các phương pháp chuẩn hóa hơn như REST hoặc GraphQL có lẽ là lựa chọn tốt hơn. Chúng cung cấp các giao thức đã được thiết lập và quen thuộc cho các hệ thống khác nhau (mà bạn không kiểm soát) để giao tiếp với nhau.

Có lẽ đã đến lúc Thử tRPC?

Nếu bạn đang xây dựng các ứng dụng full-stack với TypeScript và cảm thấy mình đang dành quá nhiều thời gian chỉ để đồng bộ hóa kiểu dữ liệu giữa frontend và backend, tRPC rất đáng để bạn tìm hiểu. Nó chắc chắn có thể làm cho quy trình phát triển của bạn mượt mà và nhanh hơn – đặc biệt quan trọng khi bạn cần xây dựng và mở rộng các ứng dụng thực tế mà không muốn lãng phí thời gian vào việc lặp lại định nghĩa kiểu hay gỡ lỗi các sự không khớp.

Chỉ mục