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>
</>
);
}