Skip to content

A starter and guide for building type-safe fullstack apps with tRPC and TanStack Query in Next.js 15 App Router. Includes best practices, code samples, and step-by-step instructions for scalable, maintainable, and modern TypeScript projects

License

Notifications You must be signed in to change notification settings

tant/nextjs15-trpc-tanstack-guide

Repository files navigation

tRPC & TanStack Query Starter for Next.js 15 App Router

A modern, type-safe fullstack starter and guide for building scalable apps with tRPC and TanStack Query in Next.js 15 App Router. Includes best practices, code samples, and step-by-step instructions for maintainable TypeScript projects.

Hướng dẫn triển khai tRPC với TanStack Query trong Next.js App Router

Tài liệu tham khảo: tRPC Next.js Setup


1. Cài đặt các package cần thiết

pnpm add @trpc/server @trpc/client @trpc/react-query @tanstack/react-query zod superjson

2. Cấu hình TypeScript strict mode (nên bật)

Trong tsconfig.json:

"compilerOptions": {
  "strict": true
}

3. Tổ chức thư mục dự án (gợi ý)

src/
  types/              # Định nghĩa các type/model dùng chung (Post, User, ...)
    post.ts
    user.ts
  app/                # Next.js App Router
    _trpc/            # Client: tRPC hooks, provider
      client.ts
      TRPCProvider.tsx
    api/
      trpc/[trpc]/route.ts # API handler cho tRPC
    layout.tsx        # Bọc app trong TRPCProvider
    page.tsx          # Sử dụng hooks tRPC
  server/
    api/
      routers/
        post.ts       # Định nghĩa router cho resource (post, user...)
        user.ts
      root.ts         # Tổng hợp các router nhỏ
      trpc.ts         # Khởi tạo tRPC, context, transformer

4. Khởi tạo tRPC backend

src/server/api/trpc.ts

  • Định nghĩa context (có thể truyền db, session, headers...)
  • Khởi tạo tRPC với transformer (superjson) và errorFormatter (Zod)
  • Xuất các hàm tạo router, procedure
import { initTRPC } from '@trpc/server';
import superjson from 'superjson';
import { ZodError } from 'zod';

export const createContext = async (opts: { headers: Headers }) => ({ ...opts });

const t = initTRPC.context<typeof createContext>().create({
  transformer: superjson,
  errorFormatter({ shape, error }) {
    return {
      ...shape,
      data: {
        ...shape.data,
        zodError: error.cause instanceof ZodError ? error.cause.flatten() : null,
      },
    };
  },
});

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

5. Định nghĩa các router nhỏ (ví dụ: post)

src/server/api/routers/post.ts

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

const fakePosts = [
  { id: 1, title: 'Chào mừng đến với tRPC', content: 'Đây là bài viết đầu tiên.' },
];

export const postRouter = createTRPCRouter({
  getAll: publicProcedure.query(() => fakePosts),
  create: publicProcedure
    .input(z.object({ title: z.string().min(1), content: z.string().min(5) }))
    .mutation(({ input }) => {
      const newPost = { id: fakePosts.length + 1, ...input };
      fakePosts.push(newPost);
      return newPost;
    }),
});

6. Tổng hợp các router nhỏ thành appRouter

src/server/api/root.ts

import { postRouter } from './routers/post';
import { createTRPCRouter } from './trpc';

export const appRouter = createTRPCRouter({
  post: postRouter,
});
export type AppRouter = typeof appRouter;

7. Tạo API handler cho tRPC

src/app/api/trpc/[trpc]/route.ts

import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { type NextRequest } from 'next/server';
import { appRouter } from '@/server/api/root';
import { createContext } from '@/server/api/trpc';

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

export { handler as GET, handler as POST };

8. Tạo tRPC client và provider phía client

src/app/_trpc/client.ts

import { createTRPCReact } from '@trpc/react-query';
import { type AppRouter } from '@/server/api/root';
export const api = createTRPCReact<AppRouter>({});

src/app/_trpc/TRPCProvider.tsx

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

export default function TRPCProvider({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient({}));
  const [trpcClient] = useState(() =>
    api.createClient({
      links: [
        httpBatchLink({
          url: '/api/trpc',
          transformer: superjson,
        }),
      ],
    })
  );
  return (
    <api.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
    </api.Provider>
  );
}

9. Bọc toàn bộ app trong TRPCProvider

src/app/layout.tsx

import TRPCProvider from '@/app/_trpc/TRPCProvider';
// ...
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <TRPCProvider>
          {children}
        </TRPCProvider>
      </body>
    </html>
  );
}

10. Sử dụng hooks tRPC trong Client Component

src/app/page.tsx

"use client";
import { useState } from 'react';
import { api } from './_trpc/client';

export default function Home() {
  const { data: posts, isLoading, isError, refetch } = api.post.getAll.useQuery();
  const createPost = api.post.create.useMutation({ onSuccess: refetch });
  // ...
}

11. Mở rộng: xác thực, middleware, error handling...

  • Tham khảo thêm: tRPC docs
  • Có thể thêm middleware, xác thực, custom context, ...

12. Một số lưu ý

  • Luôn bọc app trong TRPCProvider ở layout.tsx.
  • Đảm bảo các import sử dụng alias @ nếu đã cấu hình trong tsconfig.json.
  • Sử dụng Zod để validate input cho mutation.
  • Có thể mở rộng router, context, middleware tuỳ ý.

13. Hướng dẫn mở rộng: Thêm resource mới (ví dụ: user)

Giả sử bạn muốn thêm API cho user (bên cạnh post), hãy làm theo các bước sau:

Bước 1: Tạo router mới cho user

src/server/api/routers/user.ts

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

const fakeUsers = [
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' },
];

export const userRouter = createTRPCRouter({
  getAll: publicProcedure.query(() => fakeUsers),
  create: publicProcedure
    .input(z.object({ name: z.string().min(1) }))
    .mutation(({ input }) => {
      const newUser = { id: fakeUsers.length + 1, ...input };
      fakeUsers.push(newUser);
      return newUser;
    }),
});

Bước 2: Tổng hợp router user vào appRouter

src/server/api/root.ts

import { postRouter } from './routers/post';
import { userRouter } from './routers/user'; // <-- thêm dòng này
import { createTRPCRouter } from './trpc';

export const appRouter = createTRPCRouter({
  post: postRouter,
  user: userRouter, // <-- thêm dòng này
});
export type AppRouter = typeof appRouter;

Bước 3: Sử dụng hooks user ở phía client

src/app/page.tsx (hoặc component khác)

import { api } from './_trpc/client';

// Lấy danh sách user
const { data: users } = api.user.getAll.useQuery();

// Tạo user mới
const createUser = api.user.create.useMutation();

Bạn có thể lặp lại các bước này để mở rộng cho bất kỳ resource nào (comment, product, ...).


14. Hướng dẫn tích hợp database (ví dụ: Firebase Firestore)

Nếu bạn muốn dùng database thực tế như Firebase Firestore (hoặc bất kỳ DB nào), bạn sẽ thực hiện các thao tác truy vấn/thêm/sửa/xoá dữ liệu bên trong các procedure của router (ví dụ: trong file post.ts, user.ts, ...).

Ví dụ tích hợp Firestore vào router:

Cài đặt Firebase SDK:

pnpm add firebase

Tạo file cấu hình Firestore (ví dụ: src/server/db/firestore.ts)

import { initializeApp, getApps } from 'firebase/app';
import { getFirestore } from 'firebase/firestore';

const firebaseConfig = {
  apiKey: 'YOUR_API_KEY',
  authDomain: 'YOUR_AUTH_DOMAIN',
  projectId: 'YOUR_PROJECT_ID',
  // ...
};

const app = getApps().length ? getApps()[0] : initializeApp(firebaseConfig);
export const db = getFirestore(app);

Sử dụng Firestore trong router (ví dụ: src/server/api/routers/post.ts)

import { collection, getDocs, addDoc } from 'firebase/firestore';
import { db } from '../../db/firestore';
// ...
export const postRouter = createTRPCRouter({
  getAll: publicProcedure.query(async () => {
    const snapshot = await getDocs(collection(db, 'posts'));
    return snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
  }),
  create: publicProcedure
    .input(z.object({ title: z.string().min(1), content: z.string().min(5) }))
    .mutation(async ({ input }) => {
      const docRef = await addDoc(collection(db, 'posts'), input);
      return { id: docRef.id, ...input };
    }),
});

Lưu ý:

  • Bạn có thể truyền các instance kết nối DB qua context nếu muốn dùng cho nhiều router.
  • Với các DB khác (Prisma, MongoDB, ...), bạn cũng sẽ viết code thao tác DB ngay trong các procedure của router tương tự như trên.

3.1. Định nghĩa các type/model dùng chung (bắt buộc nên có)

Hầu hết dự án thực tế đều cần quản lý dữ liệu có cấu trúc (Post, User, ...). Để type-safe toàn dự án, bạn nên định nghĩa các type/model dùng chung ngay từ đầu.

Tạo thư mục src/types/ và định nghĩa các model:

src/types/post.ts

export type Post = {
  id: number;
  title: string;
  content: string;
};

src/types/user.ts

export type User = {
  id: number;
  name: string;
};

Sau đó, bạn import các type này vào router, client, hoặc bất kỳ đâu cần type-safe.


License

MIT License. See LICENSE for details.

About

A starter and guide for building type-safe fullstack apps with tRPC and TanStack Query in Next.js 15 App Router. Includes best practices, code samples, and step-by-step instructions for scalable, maintainable, and modern TypeScript projects

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published