Getting Started with Next.js

tRPC + Next.js File Structure#

Recommended but not enforced file structure. This is what you get when starting from the examples.

โ”œโ”€โ”€ pages
โ”‚ย ย  โ”œโ”€โ”€ _app.tsx # <-- wrap App with `withTRPC()`
โ”‚ย ย  โ”œโ”€โ”€ api
โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ trpc
โ”‚ย ย  โ”‚ย ย  ย ย  โ”œโ”€โ”€ [trpc].ts # <-- tRPC response handler
โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ [...] # <-- potential sub-routers
โ”‚ย ย  โ””โ”€โ”€ [...]
โ”œโ”€โ”€ prisma # <-- (optional) if prisma is added
โ”‚ย ย  โ”œโ”€โ”€ migrations
โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ [...]
โ”‚ย ย  โ””โ”€โ”€ schema.prisma
โ”œโ”€โ”€ public
โ”‚ย ย  โ””โ”€โ”€ [...]
โ”œโ”€โ”€ test # <-- (optional) E2E-test helpers
โ”‚ย ย  โ””โ”€โ”€ playwright.test.ts
โ”œโ”€โ”€ utils
โ”‚ย ย  โ””โ”€โ”€ trpc.ts # <-- create your typesafe tRPC hooks
โ””โ”€โ”€ [...]

๐ŸŒŸ Start from scratch#

Execute create-next-app to bootstrap one of the examples:

TodoMVC with Prisma#

TodoMVC-app implemented with tRPC. Uses superjson to transparently use Dates over the wire.

Live demo at todomvc.trpc.io

npx create-next-app --example https://github.com/trpc/trpc --example-path examples/next-prisma-todomvc trpc-todo

Simple Starter without database#

Simple starter project with a mock in-memory db.

Live demo at hello-world.trpc.io (note that data isn't persisted on Vercel as it's running in lambda functions)

npx create-next-app --example https://github.com/trpc/trpc --example-path examples/next-hello-world my-app

Real-time chat with Prisma#

Using experimental subscription support.

Live demo at chat.trpc.io

npx create-next-app --example https://github.com/trpc/trpc --example-path examples/next-ssg-chat my-chat-app

๐Ÿป Add to existing project#

The code here is taken from ./examples/next-hello-world.

0. Install deps#

yarn add @trpc/client @trpc/server @trpc/react @trpc/next zod react-query
  • tRPC wraps a tiny layer of sugar around react-query when using React which gives you type safety and auto completion of your procedures
  • Zod is a great validation lib that works well, but tRPC also works out-of-the-box with yup/myzod/[..] - see test suite

1. Create an API handler#

Create a file at ./pages/api/trpc/[trpc].ts

Paste the following code:

import * as trpc from '@trpc/server';
import * as trpcNext from '@trpc/server/adapters/next';
import * as z from 'zod';
// The app's context - is generated for each incoming request
export type Context = {};
const createContext = ({
req,
res,
}: trpcNext.CreateNextContextOptions): Context => {
return {};
};
function createRouter() {
return trpc.router<Context>();
}
// Important: only use this export with SSR/SSG
export const appRouter = createRouter()
// Create procedure at path 'hello'
.query('hello', {
// using zod schema to validate and infer input values
input: z
.object({
text: z.string().optional(),
})
.optional(),
resolve({ input }) {
// the `input` here is parsed by the parser passed in `input` the type inferred
return {
greeting: `hello ${input?.text ?? 'world'}`,
};
},
});
// Exporting type _type_ AppRouter only exposes types that can be used for inference
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export
export type AppRouter = typeof appRouter;
// export API handler
export default trpcNext.createNextApiHandler({
router: appRouter,
createContext,
});

Option A) Using Server-side rendering#

2. Create tRPC-hooks#

Create ./utils/trpc.ts

import { createReactQueryHooks } from '@trpc/react';
// Type-only import:
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export
import type { AppRouter } from '../pages/api/trpc/[trpc]';
export const trpc = createReactQueryHooks<AppRouter>();

3. Configure _app.tsx#

import { withTRPC } from '@trpc/next';
import { AppType } from 'next/dist/next-server/lib/utils';
import React from 'react';
import type { AppRouter } from './api/trpc/[trpc]';
const MyApp: AppType = ({ Component, pageProps }) => {
return <Component {...pageProps} />;
};
export default withTRPC<AppRouter>(
({ ctx }) => {
if (process.browser) {
return {
url: '/api/trpc',
};
}
// optional: use SSG-caching for each rendered page (see caching section for more details)
const ONE_DAY_SECONDS = 60 * 60 * 24;
ctx?.res?.setHeader(
'Cache-Control',
`s-maxage=1, stale-while-revalidate=${ONE_DAY_SECONDS}`,
);
// The server needs to know your app's full url
// On render.com you can use `http://${process.env.RENDER_INTERNAL_HOSTNAME}:${process.env.PORT}/api/trpc`
const url = process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}/api/trpc`
: 'http://localhost:3000/api/trpc';
return {
url,
getHeaders() {
return {
'x-ssr': '1',
};
},
};
},
{ ssr: true },
)(MyApp);

4. Start consuming your data!#

import Head from 'next/head';
import { trpc } from '../utils/trpc';
export default function Home() {
// try typing here to see that you get autocompletion & type safety on the procedure's name
const helloNoArgs = trpc.useQuery(['hello']);
const helloWithArgs = trpc.useQuery(['hello', { text: 'client' }]);
return (
<div>
<Head>
<title>Hello tRPC</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<h1>Hello World Example</h1>
<ul>
<li>
helloNoArgs ({helloNoArgs.status}):{' '}
<pre>{JSON.stringify(helloNoArgs.data, null, 2)}</pre>
</li>
<li>
helloWithArgs ({helloWithArgs.status}):{' '}
<pre>{JSON.stringify(helloWithArgs.data, null, 2)}</pre>
</li>
</ul>
</div>
);
}

Option B) Using SSG#

2. Create tRPC-hooks#

Create ./utils/trpc.ts

import { createReactQueryHooks, CreateTRPCClientOptions } from '@trpc/react';
import type { inferProcedureOutput } from '@trpc/server';
import superjson from 'superjson';
// โ„น๏ธ Type-only import:
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export
import type { AppRouter } from '../pages/api/trpc/[trpc]';
// create react query hooks for trpc
export const trpc = createReactQueryHooks<AppRouter>();

3. Configure _app.tsx#

import { withTRPC } from '@trpc/next';
import { AppType } from 'next/dist/next-server/lib/utils';
import { trpcClientOptions } from '../utils/trpc';
const MyApp: AppType = ({ Component, pageProps }) => {
return <Component {...pageProps} />;
};
export default withTRPC(
() => {
return { ...trpcClientOptions };
},
{
ssr: false,
},
)(MyApp);

4. Start consuming your data!#

import Head from 'next/head';
import { trpc } from '../utils/trpc';
import { createSSGHelpers } from '@trpc/react/ssg';
export default function Home() {
// try typing here to see that you get autocompletion & type safety on the procedure's name
const helloNoArgs = trpc.useQuery(['hello']);
const helloWithArgs = trpc.useQuery(['hello', { text: 'client' }]);
return (
<div>
<Head>
<title>Hello tRPC</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<h1>Hello World Example</h1>
<ul>
<li>
helloNoArgs ({helloNoArgs.status}):{' '}
<pre>{JSON.stringify(helloNoArgs.data, null, 2)}</pre>
</li>
<li>
helloWithArgs ({helloWithArgs.status}):{' '}
<pre>{JSON.stringify(helloWithArgs.data, null, 2)}</pre>
</li>
</ul>
</div>
);
}
// Optional: statically fetch the data
// export const getStaticProps = async (
// context: GetStaticPropsContext<{ filter: string }>,
// ) => {
// const ssg = createSSGHelpers({
// router: appRouter,
// transformer,
// ctx: {},
// });
// await ssg.fetchQuery('hello');
// await ssg.fetchQuery('hello', { text: 'client' });
// return {
// props: {
// trpcState: ssg.dehydrate(),
// },
// revalidate: 1,
// };
// };