Understanding tRPC using Next.js
What is tRPC?
tRPC stands for TypeScript Remote Procedure Call, which provides end-to-end type safety for your API endpoints.
Setting up tRPC & Next.js
Create a new Next.js app using the following command. Make sure to use the *pages* directory instead of *app*.
npx create-next-app@latest
Now install the following dependencies:
npm install @trpc/server @trpc/client @trpc/react-query @trpc/next @tanstack/react-query zod
Here, zod
is used for validating the inputs we give to our endpoints/procedures.
Initialize tRPC
Inside the Next.js api routes, create a folder named trpc
and a dynamic route inside it [trpc].ts
.
Inside this file, create your root router (appRouter
) and define the endpoints, similar to REST endpoints:
import { initTRPC } from "@trpc/server";
import * as trpcNext from "@trpc/server/adapters/next";
import { z } from "zod";
const t = initTRPC.create();
const appRouter = t.router({
greeting: t.procedure
.input(
z
.object({
name: z.string().nullish(),
})
.nullish() // allows `null` / `undefined` values in the schema
)
.query(({ input }) => {
return {
text: `hello ${input?.name ?? "world"}`,
};
}),
});
The endpoint will look like /api/trpc/greeting
.
We are using zod
for the input validation. By first creating the object schema, & setting name
to the string
schema.
The query
is basically an HTTP GET
request which takes a resolve function with input
that we defined in the procedure above (greeting
). We can use this resolve function to return some stuff, a string in this case.
Now below the appRouter
, we now export its type and an API handler.
export type AppRouter = typeof appRouter;
export default trpcNext.createNextApiHandler({
router: appRouter,
createContext: () => ({}),
});
Create tRPC hooks & Batching
Create a utils
folder on the same level as pages
and a file inside it trpc.ts
. Inside this file, we are creating a getBaseUrl
which returns the base URL based on the specific env
.
We will use trpc's createTRPCNext
function to create type-safe hooks.
import { httpBatchLink } from "@trpc/client";
import { createTRPCNext } from "@trpc/next";
import type { AppRouter } from "@/pages/api/trpc/[trpc]";
function getBaseUrl() {
if (typeof window !== "undefined") {
return "";
}
if (process.env.VERCEL_URL) {
return `https://${process.env.VERCEL_URL}`;
}
return `http://localhost:${process.env.PORT ?? 3000}`;
}
export const trpc = createTRPCNext<AppRouter>({
config({ ctx }) {
return {
links: [
httpBatchLink({
url: getBaseUrl() + "/api/trpc",
}),
],
};
},
// ssr: true,
});
Here, httpBatchLink
can take multiple URLs as an object parameter & grip them into one request. It is generally a good practice to handle multiple queries in this way.
We can now use this exported trpc
to call procedures & queries on the client-side. But before that, we need to configure the _app.tsx
file.
Configure _app.tsx
Export MyApp
and wrap it in trpc.withTRPC
import { trpc } from "@/utils/trpc";
import type { AppProps, AppType } from "next/app";
const MyApp: AppType = ({ Component, pageProps }: AppProps) => {
return <Component {...pageProps} />;
};
export default trpc.withTRPC(MyApp);
Client Side API consumption
Now, inside index.tsx
, you can import trpc
from util directory and make a call to the greeting
procedure, providing the input name
, as show below
import { trpc } from "@/utils/trpc";
export default function Home() {
/* The `name` destructure is typesafe.
* If we change the `name` in `[trpc].ts`, it will hightlight
* the error here
*/
const result = trpc.greeting.useQuery({ name: "sanyam" });
if (!result?.data) {
<div>
<h1>Loading...</h1>
</div>;
}
return (
<div>
<h1>{result?.data?.text}</h1>
</div>
);
}
Now, start your Next.js application and you can see the output being displayed.
If you head over to the network tab inside the dev tool, you can notice the HTTP GET
request with query parameters batch
& input
Directory Structure
Here is the final directory structure for reference purposes. This is the one recommended by tRPC itself.