Usage with Next.js
If you're using tRPC in a new project, consider using one of the example projects as a starting point or for reference: tRPC Example Projects
tRPC and Next.js are a match made in heaven! Next.js makes it easy for you to build your client and server together in one codebase. This makes it easy to share types between them.
tRPC includes dedicated tools to make the Next.js developer experience as seamless as possible.
Recommended file structure​
Recommended but not enforced file structure. This is what you get when starting from the examples.
graphql.├── prisma # <-- if prisma is added│ └── [..]├── src│ ├── pages│ │ ├── _app.tsx # <-- add `withTRPC()`-HOC here│ │ ├── api│ │ │ └── trpc│ │ │ └── [trpc].ts # <-- tRPC HTTP handler│ │ └── [..]│ ├── server│ │ ├── routers│ │ │ ├── _app.ts # <-- main app router│ │ │ ├── post.ts # <-- sub routers│ │ │ └── [..]│ │ ├── context.ts # <-- create app context│ │ └── trpc.ts # <-- procedure helpers│ └── utils│ └── trpc.ts # <-- your typesafe tRPC hooks└── [..]
graphql.├── prisma # <-- if prisma is added│ └── [..]├── src│ ├── pages│ │ ├── _app.tsx # <-- add `withTRPC()`-HOC here│ │ ├── api│ │ │ └── trpc│ │ │ └── [trpc].ts # <-- tRPC HTTP handler│ │ └── [..]│ ├── server│ │ ├── routers│ │ │ ├── _app.ts # <-- main app router│ │ │ ├── post.ts # <-- sub routers│ │ │ └── [..]│ │ ├── context.ts # <-- create app context│ │ └── trpc.ts # <-- procedure helpers│ └── utils│ └── trpc.ts # <-- your typesafe tRPC hooks└── [..]
Add tRPC to existing Next.js project​
1. Install deps​
npm
bashnpm install @trpc/server @trpc/client @trpc/react-query @trpc/next @tanstack/react-query zod
bashnpm install @trpc/server @trpc/client @trpc/react-query @trpc/next @tanstack/react-query zod
yarn
bashyarn add @trpc/server @trpc/client @trpc/react-query @trpc/next @tanstack/react-query zod
bashyarn add @trpc/server @trpc/client @trpc/react-query @trpc/next @tanstack/react-query zod
pnpm
bashpnpm add @trpc/server @trpc/client @trpc/react-query @trpc/next @tanstack/react-query zod
bashpnpm add @trpc/server @trpc/client @trpc/react-query @trpc/next @tanstack/react-query zod
Why @tanstack/react-query?​
@trpc/react-query provides a thin wrapper over @tanstack/react-query. It is required as a peer dependency.
Why Zod?​
Most examples use Zod for input validation and we highly recommended it, though it isn't required. You can use a validation library of your choice (Yup, Superstruct, io-ts, etc). In fact, any object containing a parse, create or validateSync method will work.
2. Enable strict mode​
If you want to use Zod for input validation, make sure you have enabled strict mode in your tsconfig.json:
tsconfig.jsondiff"compilerOptions": {+ "strict": true}
tsconfig.jsondiff"compilerOptions": {+ "strict": true}
If strict mode is too much, at least enable strictNullChecks:
tsconfig.jsondiff"compilerOptions": {+ "strictNullChecks": true}
tsconfig.jsondiff"compilerOptions": {+ "strictNullChecks": true}
3. Create a tRPC router​
Initialize your tRPC backend using the initTRPC function and create your first router.
The below showcased backend uses the recommended file structure, but you can keep it simple and put everything in the api-handler directly if you want.
View sample backend
server/trpc.tstsimport { TRPCError, initTRPC } from '@trpc/server';// Avoid exporting the entire t-object since it's not very// descriptive and can be confusing to newcomers used to t// meaning translation in i18n libraries.const t = initTRPC.create();// Base router and procedure helpersexport const router = t.router;export const publicProcedure = t.procedure;/*** Reusable middleware that checks if users are authenticated.* @note Example only, yours may vary depending on how your auth is setup**/const isAuthed = t.middleware(({ next, ctx }) => {if (!ctx.session?.user?.email) {throw new TRPCError({code: 'UNAUTHORIZED',});}return next({ctx: {// Infers the `session` as non-nullablesession: ctx.session,},});});// Protected procedures for logged in users onlyexport const protectedProcedure = t.procedure.use(isAuthed);
server/trpc.tstsimport { TRPCError, initTRPC } from '@trpc/server';// Avoid exporting the entire t-object since it's not very// descriptive and can be confusing to newcomers used to t// meaning translation in i18n libraries.const t = initTRPC.create();// Base router and procedure helpersexport const router = t.router;export const publicProcedure = t.procedure;/*** Reusable middleware that checks if users are authenticated.* @note Example only, yours may vary depending on how your auth is setup**/const isAuthed = t.middleware(({ next, ctx }) => {if (!ctx.session?.user?.email) {throw new TRPCError({code: 'UNAUTHORIZED',});}return next({ctx: {// Infers the `session` as non-nullablesession: ctx.session,},});});// Protected procedures for logged in users onlyexport const protectedProcedure = t.procedure.use(isAuthed);
server/routers/_app.tstsimport { z } from 'zod';import { publicProcedure, router } from '../trpc';export const appRouter = router({hello: publicProcedure.input(z.object({text: z.string().nullish(),}),).query(({ input }) => {return {greeting: `hello ${input?.text ?? 'world'}`,};}),});// export type definition of APIexport type AppRouter = typeof appRouter;
server/routers/_app.tstsimport { z } from 'zod';import { publicProcedure, router } from '../trpc';export const appRouter = router({hello: publicProcedure.input(z.object({text: z.string().nullish(),}),).query(({ input }) => {return {greeting: `hello ${input?.text ?? 'world'}`,};}),});// export type definition of APIexport type AppRouter = typeof appRouter;
If you need to split your router into several subrouters, you can implement them in the server/routers directory and import and merge them to a single root appRouter. Check out the next-prisma-starter for an example of this usage.
pages/api/trpc/[trpc].tstsimport * as trpcNext from '@trpc/server/adapters/next';import { appRouter } from '../../../server/routers/_app';// export API handlerexport default trpcNext.createNextApiHandler({router: appRouter,createContext: () => ({}),});
pages/api/trpc/[trpc].tstsimport * as trpcNext from '@trpc/server/adapters/next';import { appRouter } from '../../../server/routers/_app';// export API handlerexport default trpcNext.createNextApiHandler({router: appRouter,createContext: () => ({}),});
4. Create tRPC hooks​
Create a set of strongly-typed hooks using your API's type signature.
utils/trpc.tstsximport { httpBatchLink } from '@trpc/client';import { createTRPCNext } from '@trpc/next';import type { AppRouter } from '../server/routers/_app';function getBaseUrl() {if (typeof window !== 'undefined')// browser should use relative pathreturn '';if (process.env.VERCEL_URL)// reference for vercel.comreturn `https://${process.env.VERCEL_URL}`;if (process.env.RENDER_INTERNAL_HOSTNAME)// reference for render.comreturn `http://${process.env.RENDER_INTERNAL_HOSTNAME}:${process.env.PORT}`;// assume localhostreturn `http://localhost:${process.env.PORT ?? 3000}`;}export const trpc = createTRPCNext<AppRouter>({config({ ctx }) {return {links: [httpBatchLink({/*** If you want to use SSR, you need to use the server's full URL* @link https://trpc.io/docs/ssr**/url: `${getBaseUrl()}/api/trpc`,}),],/*** @link https://tanstack.com/query/v4/docs/reference/QueryClient**/// queryClientConfig: { defaultOptions: { queries: { staleTime: 60 } } },};},/*** @link https://trpc.io/docs/ssr**/ssr: true,});// => { useQuery: ..., useMutation: ...}
utils/trpc.tstsximport { httpBatchLink } from '@trpc/client';import { createTRPCNext } from '@trpc/next';import type { AppRouter } from '../server/routers/_app';function getBaseUrl() {if (typeof window !== 'undefined')// browser should use relative pathreturn '';if (process.env.VERCEL_URL)// reference for vercel.comreturn `https://${process.env.VERCEL_URL}`;if (process.env.RENDER_INTERNAL_HOSTNAME)// reference for render.comreturn `http://${process.env.RENDER_INTERNAL_HOSTNAME}:${process.env.PORT}`;// assume localhostreturn `http://localhost:${process.env.PORT ?? 3000}`;}export const trpc = createTRPCNext<AppRouter>({config({ ctx }) {return {links: [httpBatchLink({/*** If you want to use SSR, you need to use the server's full URL* @link https://trpc.io/docs/ssr**/url: `${getBaseUrl()}/api/trpc`,}),],/*** @link https://tanstack.com/query/v4/docs/reference/QueryClient**/// queryClientConfig: { defaultOptions: { queries: { staleTime: 60 } } },};},/*** @link https://trpc.io/docs/ssr**/ssr: true,});// => { useQuery: ..., useMutation: ...}
createTRPCNext does not work with interop mode. If you are migrating from v9 using interop, keep using the old way of initializing tRPC.
5. Configure _app.tsx​
pages/_app.tsxtsximport type { AppType } from 'next/app';import { trpc } from '../utils/trpc';const MyApp: AppType = ({ Component, pageProps }) => {return <Component {...pageProps} />;};export default trpc.withTRPC(MyApp);
pages/_app.tsxtsximport type { AppType } from 'next/app';import { trpc } from '../utils/trpc';const MyApp: AppType = ({ Component, pageProps }) => {return <Component {...pageProps} />;};export default trpc.withTRPC(MyApp);
6. Make API requests​
pages/index.tsxtsximport { trpc } from '../utils/trpc';export default function IndexPage() {const hello = trpc.hello.useQuery({ text: 'client' });if (!hello.data) {return <div>Loading...</div>;}return (<div><p>{hello.data.greeting}</p></div>);}
pages/index.tsxtsximport { trpc } from '../utils/trpc';export default function IndexPage() {const hello = trpc.hello.useQuery({ text: 'client' });if (!hello.data) {return <div>Loading...</div>;}return (<div><p>{hello.data.greeting}</p></div>);}
createTRPCNext() options​
config-callback​
The config-argument is a function that returns an object that configures the tRPC and React Query clients. This function has a ctx input that gives you access to the Next.js req object, among other things. The returned value can contain the following properties:
- Required:
linksto customize the flow of data between tRPC Client and the tRPC Server. Read more.
- Optional:
queryClientConfig: a configuration object for the React QueryQueryClientused internally by the tRPC React hooks: QueryClient docsqueryClient: a React Query QueryClient instance- Note:: You can only provide either a
queryClientor aqueryClientConfig.
- Note:: You can only provide either a
transformer: a transformer applied to outgoing payloads. Read more about Data TransformersabortOnUnmount: determines if in-flight requests will be cancelled on component unmount. This defaults tofalse.
unstable_overrides: (default: undefined)​
Configure overrides for React Query's hooks.
ssr-boolean (default: false)​
Whether tRPC should await queries when server-side rendering a page. Defaults to false.
responseMeta-callback​
Ability to set request headers and HTTP status when server-side rendering.
Example​
utils/trpc.tstsximport { createTRPCNext } from '@trpc/next';import type { AppRouter } from '../pages/api/trpc/[trpc]';export const trpc = createTRPCNext<AppRouter>({config({ ctx }) {/* [...] */},ssr: true,responseMeta({ clientErrors, ctx }) {if (clientErrors.length) {// propagate first http error from API callsreturn {status: clientErrors[0].data?.httpStatus ?? 500,};}// cache full page for 1 day + revalidate once every secondconst ONE_DAY_IN_SECONDS = 60 * 60 * 24;return {'Cache-Control': `s-maxage=1, stale-while-revalidate=${ONE_DAY_IN_SECONDS}`,};},});
utils/trpc.tstsximport { createTRPCNext } from '@trpc/next';import type { AppRouter } from '../pages/api/trpc/[trpc]';export const trpc = createTRPCNext<AppRouter>({config({ ctx }) {/* [...] */},ssr: true,responseMeta({ clientErrors, ctx }) {if (clientErrors.length) {// propagate first http error from API callsreturn {status: clientErrors[0].data?.httpStatus ?? 500,};}// cache full page for 1 day + revalidate once every secondconst ONE_DAY_IN_SECONDS = 60 * 60 * 24;return {'Cache-Control': `s-maxage=1, stale-while-revalidate=${ONE_DAY_IN_SECONDS}`,};},});
Next steps​
Refer to the @trpc/react-query docs for additional information on executing Queries and Mutations inside your components.