NextJs 14 with tRPC

Introduce

In this article, we are not supposed to research how to use trpc particularly specific, but how to build a simple nextjs14 project with tRPC.

If you don't know what is the tRPC at all, please read trpc.io/

My Opinion about tRPC

It likes REST API. They are both used to communicate between frontend and backend. such as requesting and responsing.

However, there are some differences from them as following:

example: http://xxx/com/

  • REST API
Client Server
axios.get('/api/user') GET /api/user -> getUsers()
axios.post('/api/user') POST /api/user -> addUser(user)
axios.put('/api/user') PUT /api/user -> updateUser(user)
axios.delete('/api/user') DELETE /api/user -> deleteUser(id)
  • tRPC
Client Server
trpcClient.getUsers.query() -> getUsers()
trpcClient.addUser.mutate() -> addUser(user)
trpcClient.updateUser.mutate() -> updateUser(user)
trpcClient.deleteUser.mutate() -> deleteUser(id)

We should write function in order to process the client request. and call the server function directly, not to concatenat request address any more. That make coding eaaier.

Feature about tRPC

  • HttpRequest's method and URL are not required, and the client calls the server function (RPC) directly.
  • Multiple requests (RPC) on the same page are combined into a single request.
  • No dependence. Small size.
  • It can be used by both old and new project and is suitable for a variety of popular frameworks,such as nextjs、express、koa and so on.
  • In REST API: Client (TS), Server (Any-JAVA\PHP\NODE\GO...)
  • In tRPC: Client (TS), Server (TS)

Starter

Environmental preparation

perl 复制代码
bunx create-next-app@latest next-trpc

bun add @trpc/server@next @trpc/client@next @trpc/react-query@next @trpc/next@next @tanstack/react-query@latest zod

Structure

  • server:trpc-router,RPC function

Tsconfig

json 复制代码
{
  "compilerOptions": {
      // ...
      "strictNullChecks": true,
      // ...
  }
}

Server

In the root of your project, create a folder named "server".

Create tRPC Server

typescript 复制代码
// /server/trpc.ts
import { initTRPC } from '@trpc/server';
import { Context } from './context';

const t = initTRPC.context<Context>().create();

export const router = t.router; // using to create router
export const procedure = t.procedure; // using to create function
// @see https://trpc.io/docs/server/server-side-calls#create-caller
export const createCallerFactory = t.createCallerFactory;

Create Context

typescript 复制代码
// /server/context.ts
import { FetchCreateContextFnOptions } from '@trpc/server/adapters/fetch';

type Opts = Partial<FetchCreateContextFnOptions>;

/**
 * @see https://trpc.io/docs/server/adapters/fetch#create-the-context
 */
export function createContext(opts?: Opts): Opts & {
	user?: { id: string };
} {
	// const user = { name: req.headers.get('user') ?? 'anonymous' };
	const user = { id: 'test123' };
	console.log('-----', { ...(opts || {}), user });
	return { ...(opts || {}), user };
}

export type Context = Awaited<ReturnType<typeof createContext>>;

Create tRPC Router

  • Create business router
typescript 复制代码
// server/routers/user.ts
import { z } from 'zod';
import { procedure, router } from '../trpc';
import { TRPCError } from '@trpc/server';

export const User = router({
	getUserList: procedure.query(() => {
		// Get data from Database
		const users = [
			{ id: 1, name: 'Xfz', age: 15 },
			{ id: 2, name: 'Xwb', age: 18 },
			{ id: 3, name: 'Zc', age: 20 },
			{ id: 4, name: 'Xbz', age: 25 },
		];
		return users;
	}),
	createUser: procedure
		.input(
			z.object({
				id: z.number(),
				name: z.string(),
				age: z.number(),
			})
		)
		.mutation(async opt => {
			console.log(opt.input);
			console.log(opt.ctx.user);
			try {
				// Insert data into Database
				return {
					status: 200,
					data: {},
					message: 'Created Success',
				};
			} catch (error: any) {
				console.log(error.message);
				throw new TRPCError({
					code: 'INTERNAL_SERVER_ERROR',
					message: error.message,
				});
			}
		}),
});
  • Create entrancet to the tRPC router
typescript 复制代码
// /server/routers/index.ts
import { router } from '../trpc';
import { User } from './user';

export const appRouter = router({
	User,
	// other router
});

export type AppRouter = typeof appRouter;

Client

Create tRPC client and tRPC serverClient

  • In app, create a folder named "_trpc" using to store trpc's client
typescript 复制代码
// app/_trpc.client.ts
import { createTRPCReact } from '@trpc/react-query';

import type { AppRouter } from '@/server/routers';

export const trpc = createTRPCReact<AppRouter>({});
  • As we use the "App Router" from nextjs14, there is not _app.tsx. then we can not use "withTRPC" to wrap app component. so we should offer a provider to deal with tRPC.
tsx 复制代码
// app/_trpc/Provider.tsx
'use client'; // here here here!!!!
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import React, { useState } from 'react';
import { trpc } from './client';
import { getBaseUrl } from '@/lib/trpc';

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

// lib/trpc.ts 
export const getBaseUrl = () => {
	if (typeof window !== 'undefined')
		// browser should use relative path
		return '';
	if (process.env.VERCEL_URL)
		// reference for vercel.com
		return `https://${process.env.VERCEL_URL}`;
	if (process.env.RENDER_INTERNAL_HOSTNAME)
		// reference for render.com
		return `http://${process.env.RENDER_INTERNAL_HOSTNAME}:${process.env.PORT}`;
	// assume localhost
	return `http://localhost:${process.env.PORT ?? 3000}`;
};
  • In Layout wrap compoent with Provider
tsx 复制代码
/app/layout.tsx
// ......
import Provider from '@/app/_trpc/Provider';
// .....

export default function RootLayout({children,}: Readonly<{children: React.ReactNode}>) {
	return (
		<html lang="en">
			<body className={inter.className}>
				<Provider>{children}</Provider>
			</body>
		</html>
	);
}
  • Create API router
typescript 复制代码
// /app/api/trpc/[trpc]/routes
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from '@/server/routers';

function handler(req: Request) {
	return fetchRequestHandler({
		endpoint: '/api/trpc',
		req,
		router: appRouter,
		createContext: ctx => ({}),
	});
}
export { handler as GET, handler as POST };

Using in Clinet Component

  • Attention please:'use client'
tsx 复制代码
// /app/page.tsx
'use client';
import { trpc } from './_trpc/client';
export default function Home() {
	// RPC 
	const { mutate: createUser } = trpc.User.createUser.useMutation();

	const clickHandler = () => {
		createUser({
			id: 5,
			name: 'Twins',
			age: 30,
		});
	};
        
        // RPC
	const userList = trpc.User.getUserList.useQuery().data;
	if (!userList) return <div>Loading</div>;

	return (
		<>
			{userList.map(user => (
				<div key={user.id}>
					{user.name} - {user.age}
				</div>
			))}
			<button onClick={clickHandler}>Create User</button>
		</>
	);
}

Using in Server Component

  • Create Server Client
ts 复制代码
/app/_trc/serverClient.ts
import { getBaseUrl } from '@/lib/trpc';
import { appRouter } from '@/server/routers';
import { createCallerFactory } from '@/server/trpc';
import { httpBatchLink } from '@trpc/client';

const createCaller = createCallerFactory(appRouter);

export const STRPC = createCaller({
	links: [
		httpBatchLink({
			url: `${getBaseUrl()}/api/trpc`,
			async headers() {
				return {};
			},
		}),
	],
});
  • Create a Server Component
tsx 复制代码
/app/stest/page.tsx
import { STRPC } from '@/app/_trpc/serverClient';

export default async function Home() {
	const userList = await STRPC.User.getUserList();

	const createAction = async () => {
		'use server'; // here here here
		await STRPC.User.createUser({
			id: 5,
			name: 'Jack',
			age: 6,
		});
	};

	return (
		<>
			{userList.map(user => (
				<div key={user.id}>
					{user.name} - {user.age}
				</div>
			))}
			<form action={createAction}>
				<button>Create User</button>
			</form>
		</>
	);
}
相关推荐
meilindehuzi_a1 小时前
Node.js × 大模型:AIGC 工程化基础与异步流控总结
node.js·aigc
不好听61315 小时前
Node.js 工程化开发流程 — 知识点总结
javascript·node.js
喵个咪17 小时前
基于 Next.js 的 Headless CMS 前端架构:技术解析与二次开发导引
前端·react.js·next.js
HjhIron18 小时前
🚀 从零开始,用 Node.js 构建你的第一个 AIGC 项目
node.js·aigc
To_OC19 小时前
我调用 DeepSeek API 连踩 3 个坑,终于把 Node AIGC 开发的核心知识点捋顺了
后端·node.js·aigc
倾颜20 小时前
React 自定义 Hook 实战:把 AI Chat 的会话流和滚动体验从组件中拆出来
前端·react.js·next.js
sugar__salt1 天前
从零落地 Generative AI 接口调用:Node.js 工程化最佳实践
人工智能·node.js
JieE2121 天前
AIGC 工程化入门:从零搭建你的第一个 AI 应用 -- 以 DeepSeek API 为例,带你走通大模型应用开发的完整链路
node.js·aigc·ai编程
moMo1 天前
藏好Key的小妙招,从搭建node.js+大模型项目开始
人工智能·node.js
书中枫叶1 天前
生活缴费充值系统
前端·javascript·经验分享·mongodb·node.js