How to set up NextJS app router, tRPC, and React Query

How to set up NextJS app router, tRPC, and React Query

Published at

I recently wanted to set up a NextJS project with tRPC and React Query, however, the tRPC documentation wasn’t as clear as it needed to be, and it didn’t have the specific details on how to set up NextJS with tRPC and React query.

So, in this tutorial, we’re going to be doing a few things:

  • We’re going to set up a Monorepo for the NodeJS backend and the NextJS frontend projects.
  • We’re going to set up a tRPC server using NodeJS.
  • We’re going to set up a NextJS project V14.x.
  • We’re going to set up a tRPC client for the NextJS frontend.

So, let’s get started.

Setting up the NextJS/NodeJS monorepo

We need to have a Monorepo so that the frontend and the backend can re-use code and use each others types and interfaces.

For this case, we only need to define the tRPC router from the backend and export the app router type, so that the frontend project can import that type and re-use it in the frontend.

This will help in having a type-safe NextJS frontend, so that our application is completely type-safe and we’ll never have to worry about that class of errors again, it will also help in autocomplete features, making front-end development a breeze.

This also helps in reducing the code written in a project, previously we would’ve written a function to utilize axios or the fetch API in the front end to send a request to the backend API, we’d have to create the type interfaces in the front-end to get a type-safe app.

But the problem with that was, if the backend changed during development and the API response was something different from the previous one, there would be no way to know that the API has changed and we’d have to manually update the front-end too.

This would make maintenance really difficult and the project would be really prone to many bugs and errors in its development lifecycle.

With that being said, let’s set up the Monorepo and see its true powers. We’re going to use PNPM to set up the workspace, PNPM is a fast disk-space efficient package manager, and I love it because of its speed and efficiency.

Let’s create a directory for the project, this will include the backend and frontend code too.

mkdir next-trpc

cd into the project and initialize an PNPM project:

cd next-trpc

pnpm init

pnpm init

This will create a package.json in the root directory.

Let’s create a pnpm-workspace.yaml file in the root directory and define the packages that we’re going to have in the project, in this case our packages will be the front-end and back-end projects.

# pnpm-workspace.yaml
  - frontend
  - backend

Now, that our workspace file is created, let’s create the backend project first and then the frontend project.

Creating the NodeJS server for tRPC

Create a backend folder and cd into it

mkdir backend

cd backend/

Initialize the project

pnpm init

pnpm init

Install the necessary dependencies for a simple NodeJS, express server, and tRPC.

pnpm add @trpc/server cors dotenv express
  • @trpc/server: Is the main packages for building a tRPC server with NodeJS.
  • cors: We need this packages to let other origins send requests to the NodeJS server.
  • dotenv: For reading environment variables.
  • express: To create a simple express server and integrated it with tRPC.

Install the dev dependencies

pnpm add -D @types/cors @types/express @types/node typescript ts-node-dev
  • @types/cors: Types for the cors package.
  • @types/express: Types for the express packages.
  • @types/node: NodeJS types.
  • typescript: We need this package because we’re going to write our code in TypeScript.
  • ts-node-dev: So that we can run the server in development mode, refreshes when changes are made to the backend code.

Initialize TypeScript, create a tsconfig.json file in the backend folder:

  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true,
    "rootDir": "./src",
    "outDir": "./dist"
  "include": ["src/**/*.ts"],
  "exclude": ["node_modules", "**/*.spec.ts"]

rootDir: Specifies the root directory that we’re going to write our TypeScript code.

outDir: Specifies the root directory of where the JavaScript will be transpiled to.

Create a simple NodeJS server

Create an index.ts file in the src directory

import { config } from "dotenv"
import express from "express"
import cors from "cors"

const app = express()


const PORT = process.env.PORT || 5000

app.listen(PORT, () =>
  console.log(`Server running on port http://localhost:${PORT}`)

Update the scripts property in your packages.json file

"scripts": {
  "dev": "ts-node-dev --respawn --transpile-only src/index.ts"

Running the server

pnpm dev

pnpm dev

Setting up the tRPC server in NodeJS

import { config } from "dotenv"
import express from "express"
import cors from "cors"
import { initTRPC } from "@trpc/server"
import { createExpressMiddleware } from "@trpc/server/adapters/express"

const app = express()

type Context = {}

const createContext = (): Context => {
  // Add your context here
  return {}

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

const appRouter = t.router({
  hello: t.procedure.query(() => {
    return { message: "Hello world" }

export type AppRouter = typeof appRouter

const PORT = process.env.PORT || 5000

    router: appRouter,

app.listen(PORT, () =>
  console.log(`Server running on port http://localhost:${PORT}`)

Let’s break down the code.

  • tRPC Router: The appRouter is the main router of the tRPC server, it’s responsible to handling all the routings for it.
  • Context: The createContext function runs every-time is sent to the tRPC server, this is important to run some code before handling a route, like handling authentication and protecting routes.
  • Express middleware: The createExpressMiddleware is a function exposed by the @trpc/server library, it’s built to integrate the tRPC server with an Express server, in our case, we are sending requests to the route /trpc and tRPC then handles it’s internal routing.

This is the complete backend set up for an express.js and tRPC server, now let’s set up the NextJS frontend with app router.

Setting up NextJS 14 (App Router) with tRPC and React Router

Now our server is ready, it’s time to set up the tRPC client for our NextJS frontend.

Set up NextJS

pnpm create next-app@latest

I will name the project frontend and select all the default options

pnpm next create app

If you’re deciding to have a different name for your frontend project, make sure you update it in the pnpm-lock.yaml as well.

Here’s how mine looks like:

# pnpm-workspace.yaml
  - frontend
  - backend

Install the tRPC client dependencies for NextJS

pnpm add @tanstack/react-query @trpc/client @trpc/react-query

If you have a peer dependency issue, install the compatible version

peer dependency issue

You can fix this by installing the compatible dependency as given by PNPM

pnpm add @tanstack/react-query@^4.18.0

peer dependency fix by pnpm

After that we need to install the backend as a package so that we can use the tRPC router interface in the frontend too.

First we need to update the backend package.json

  "name": "backend",
  "main": "src/index.ts"

In your frontend package.json add the backend as a dependency

"dependencies": {
  "backend": "workspace:*"

Install it

pnpm i

pnpm install

Now that installations are done, it’s time to initialize the tRPC client.

In your frontend, create a file for the tRPC client, I will create the file in utils/trpc.ts

import { createTRPCReact } from "@trpc/react-query"
import type { AppRouter } from "backend"

export const trpc = createTRPCReact<AppRouter>()

The type AppRouter must be exported from the backend, this is going to give the tRPC client a type safe interface.

React query requires the application to be wrapped around the Query provider, but in NextJS app router we can not use client side code in server side components without using use client, and we can’t use use client in the root of our application because we do not want the entirety of our application to be rendered client side.

So, instead we can create a new file named _providers.tsx and make that a client component and accept the rest of the application using the children prop.

This will make sure the rest of the application children will be rendered server side and the provider wrapper itself is rendered client side, giving us all the react query useful features while still able to render the rest of our application from the server.

So, let’s create the providers file, create a new file in the app directory named _providers.tsx

"use client"

import { FC, ReactNode, useState } from "react"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"

import { httpBatchLink } from "@trpc/client"
import { trpc } from "@/utils/trpc"

export const Providers: FC<{ children: ReactNode }> = ({ children }) => {
  const [queryClient] = useState(() => new QueryClient())
  const [trpcClient] = useState(() =>
      links: [
          url: "http://localhost:5000/trpc",
          async headers() {
            return {
              // authorization: "Bearer ..."

  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>

Let’s break down the code:

  • Query client: This is the query client of react query, this will be passed to the tRPC provider and the Query client provider as well.
  • tRPC client: The tRPC client will be given to tRPC provider, created by using the createClient method on the trpc object.

Now let’s wrap it around the our application.

To do that, open the layout.tsx in the app directory and wrap your application with the providers we just created.

Here’s how the layout.tsx file looks like:

import type { Metadata } from "next"
import { Inter } from "next/font/google"
import "./globals.css"
import { Providers } from "./_providers"

const inter = Inter({ subsets: ["latin"] })

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",

export default function RootLayout({
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body className={inter.className}>

Great, now our tRPC client is ready to send requests to our tRPC server and it’s completely integrated with React query to handle the caching and all of it’s useful features.

Sending requests from the NextJS client to tRPC server

Now that our set up is completely done, it’s time to send some requests from the NextJS client to the tRPC server.

Currently we only have one route in our tRPC router which is the hello router we first created, so let's start sending a request to it and show it on the client page.

"use client"

import { trpc } from "@/utils/trpc"

export default function Home() {
  const { data, isLoading } = trpc.hello.useQuery()

  if (isLoading || !data) {
    return <div>Loading...</div>

  return <div className="p-24">{data.message}</div>

Nextjs app

Great, now our data is coming from the tRPC server, the tRPC interface is also strictly typed, anything changed from the server you’ll instantly know it has changed, making your application robust and reliable.


In this blog post, we delved into the crucial role of tRPC and its integration with NextJS. We also explored how incorporating React Query with tRPC enriches your development process. This integration results in a strictly typed interface, which effectively makes a wide range of potential errors a non-issue. By adopting this method, your application becomes more robust and maintainable, a key aspect for long-term project success.


We won't show your email address!

500 characters left