tRPC + Next.js: Giải pháp dứt điểm lỗi ‘lệch’ Type giữa Client và Server

Development tutorial - IT technology blog
Development tutorial - IT technology blog

Nỗi ám ảnh mang tên ‘Lệch dữ liệu’

Bạn đã bao giờ thức đến 2h sáng chỉ để tìm một lỗi undefined chưa? Kịch bản rất quen thuộc: Backend âm thầm đổi tên field từ user_id thành userId. Phía Frontend vẫn đinh ninh dùng tên cũ. TypeScript lúc này hoàn toàn “mù tịt” vì nó không biết chuyện gì đang xảy ra ở phía Server. Kết quả là ứng dụng crash ngay trên tay khách hàng.

Vấn đề cốt lõi nằm ở khoảng cách thế hệ giữa Client và Server. Dù dùng REST hay GraphQL, bạn vẫn phải định nghĩa lại Interface ở cả hai đầu. Việc này tốn thời gian và cực kỳ dễ sai sót. Thực tế, mình từng tốn 3 tiếng đồng hồ chỉ để sửa một lỗi typo trong file Swagger. Đáng lẽ công nghệ phải làm thay chúng ta việc này.

Đó là lý do tRPC trở thành cứu cánh. Nó kết nối trực tiếp Type từ Backend sang Frontend mà không cần bước trung gian. Khi bạn sửa code Server, VS Code phía Client sẽ báo đỏ ngay lập tức. Không cần chờ đến lúc chạy app mới biết mình sai.

tRPC là gì và tại sao nó lại khác biệt?

tRPC (TypeScript Remote Procedure Call) không phải là framework thần thánh gì. Nó đơn giản là một lớp vận chuyển dữ liệu thông minh. Điểm ăn tiền nhất của tRPC là không cần Schema (như .graphql) và không cần Code Generation.

Nó tận dụng tối đa cơ chế tự suy luận (Inference) của TypeScript. Khi bạn viết một hàm ở Server, Client sẽ tự biết hàm đó cần tham số gì và trả về object nào. Mọi thứ diễn ra theo thời gian thực.

Pro-tip: Khi làm việc với các object API phức tạp, mình thường dùng toolcraft.app/vi/tools/developer/json-formatter. Công cụ này giúp format dữ liệu cực nhanh để kiểm tra cấu trúc trước khi map vào logic. Nó tiện hơn nhiều so với việc cài đống extension nặng nề.

Cài đặt tRPC cho dự án Next.js

Mình giả định bạn đang dùng Next.js App Router với TypeScript. Nếu khởi tạo mới, hãy dùng npx create-next-app@latest. Tiếp theo, cài đặt các thư viện lõi của tRPC và Zod để validate dữ liệu:

npm install @trpc/client @trpc/server @trpc/react-query @trpc/next @tanstack/react-query zod

Zod sẽ đóng vai trò “người gác cổng”. Nó vừa xác thực dữ liệu đầu vào, vừa giúp tRPC hiểu cấu trúc mà Client cần gửi lên.

Thiết lập phía Server (Backend)

Đầu tiên, hãy tạo file khởi tạo tRPC. Thông thường mình đặt tại src/server/trpc.ts.

1. Khởi tạo tRPC Instance

import { initTRPC } from '@trpc/server';

const t = initTRPC.create();

export const router = t.router;
export const publicProcedure = t.procedure;

2. Định nghĩa Procedure (API Endpoints)

Thay vì viết route /api/users truyền thống, bạn sẽ định nghĩa trong Router. Tạo file src/server/routers/_app.ts:

import { z } from 'zod';
import { router, publicProcedure } from '../trpc';

export const appRouter = router({
  getUser: publicProcedure
    .input(z.object({ id: z.string() }))
    .query(async (opts) => {
      const { input } = opts;
      return {
        id: input.id,
        name: 'Nguyen Van A',
        email: '[email protected]',
      };
    }),
});

export type AppRouter = typeof appRouter;

Lưu ý dòng export type AppRouter. Đây chính là “phép màu”. Client chỉ import đúng cái Type này, tuyệt đối không import logic xử lý. Điều này giữ cho bundle size phía Frontend luôn nhẹ nhàng.

3. Tạo Route Handler

Trong App Router, bạn cần một handler để hứng request tại src/app/api/trpc/[trpc]/route.ts:

import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from '@/server/routers/_app';

const handler = (req: Request) =>
  fetchRequestHandler({
    endpoint: '/api/trpc',
    req,
    router: appRouter,
    createContext: () => ({}),
  });

export { handler as GET, handler as POST };

Kết nối phía Client (Frontend)

Bây giờ là lúc hưởng thành quả. Bạn cần tạo một utility để tRPC biết cách gọi lên Server.

1. Tạo React Hooks

Tạo file src/utils/trpc.ts:

import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '@/server/routers/_app';

export const trpc = createTRPCReact<AppRouter>();

2. Thiết lập Provider

Vì tRPC chạy trên nền TanStack Query, bạn cần bọc app trong một Provider. Hãy tạo component src/components/Provider.tsx:

'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import React, { useState } from 'react';
import { trpc } from '@/utils/trpc';

export default function Provider({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient());
  const [trpcClient] = useState(() =>
    trpc.createClient({
      links: [httpBatchLink({ url: 'http://localhost:3000/api/trpc' })],
    })
  );

  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
    </trpc.Provider>
  );
}

Trải nghiệm thực tế trong Component

Đây là lúc bạn thấy tRPC sướng thế nào. TypeScript sẽ gợi ý chính xác từng thuộc tính bạn đã viết ở Server.

'use client';
import { trpc } from '@/utils/trpc';

export default function UserProfile() {
  const userQuery = trpc.getUser.useQuery({ id: '123' });

  if (userQuery.isLoading) return <div>Đang tải...</div>;

  return (
    <div>
      <h1>{userQuery.data?.name}</h1>
      <p>Email: {userQuery.data?.email}</p>
    </div>
  );
}

Thử đổi name thành fullName ở Server xem? Lập tức dòng userQuery.data?.name ở Client sẽ báo lỗi đỏ. Bạn không cần reload hay chạy lại script nào cả. Mọi thứ đồng bộ ngay lập tức.

Tối ưu hiệu năng và Debug

Mở Network tab trong Chrome, bạn sẽ thấy các request gửi đến /api/trpc/getUser. tRPC có một tính năng cực hay là Batching. Nếu component gọi 3 API cùng lúc, tRPC tự động gộp chúng thành 1 request duy nhất. Điều này giúp giảm đáng kể độ trễ mạng.

Kinh nghiệm xương máu: Đừng dồn tất cả vào một file _app.ts. Khi dự án lớn dần, hãy chia nhỏ thành userRouter, orderRouter rồi dùng t.mergeRouters để gộp lại. Code sẽ sạch sẽ và dễ quản lý hơn nhiều.

Nếu bạn cần test các chuỗi Regex phức tạp cho Zod, hãy ghé qua toolcraft.app/vi/tools/developer/regex-tester. Nó giúp bạn kiểm tra nhanh độ chính xác của Regex mà không cần chạy lại toàn bộ server.

Lời kết

tRPC mạnh nhưng không phải là “viên đạn bạc”. Nó chỉ hoạt động tốt nhất trong môi trường Monorepo. Nếu bạn đang làm Public API cho bên thứ ba, REST hoặc GraphQL vẫn là lựa chọn an toàn hơn. Nhưng với dự án Fullstack TypeScript, tRPC thực sự là một cuộc cách mạng về năng suất. Chúc bạn có những trải nghiệm code không còn lỗi runtime!

Share: