Get End-to-End Typesafe APIs With tRPC and Next.js App Router
REST and GraphQL have been the popular choices for building and consuming APIs in the last 7-10 years. Recently, TypeScript Remote Procedure Call (tRPC) has been making a splash promising end-to-end type safe APIs. On the backend, your APIs are written as typed functions. On the frontend, your API calls will type check your function calls. In your IDE, your calls are fully typed and has autocompletion.
There are powerful options that utilizes tRPC like Create T3 App. With T3, you get more functionality but you also get a lot of files that are not necessary to get tRPC working. Furthermore, T3 is only meant to create a new project. Iβm going to walk through a basic setup of tRPC in an existing Next.js project.
1. Installing tRPC
We need to install the required tRPC packages and a couple support packages. Tanstack Query is a library that simplifies data-fetching. Zod is a TypeScript-first schema declaration and validation library that integrates well with tRPC.
pnpm add @trpc/server @trpc/client @trpc/react-query @trpc/next @tanstack/react-query zod
2. Enable Strict Mode
To use Zod for validation, add the following to tsconfig.json
:
"compilerOptions": {
+ "strict": true
}
3. File structure overview
Here is the recommended file structure for our Next.js project. I have a GitHub repo with all the files here.
.
βββ src
β βββ layout.tsx # <-- add Provider here
β βββ page.tsx # <-- make API call here
β βββ app
β β βββ api
β β β βββ trpc
β β β βββ [trpc]
β β β βββ route.ts # <-- tRPC HTTP handler
β β βββ [..]
β βββ server
β β βββ index.ts # <-- main app router
β β βββ trpc.ts # <-- procedure helpers
β βββ utils
β βββ Provider.ts # <-- create app context
β βββ trpc.ts # <-- your type safe tRPC hooks
βββ [..]
4. Create server files
server/trpc.ts
import { initTRPC } from "@trpc/server";
// Avoid exporting the entire t-object
// since it's not very descriptive.
// For instance, the use of a t variable
// is common in i18n libraries.
const t = initTRPC.create();
// Base router and procedure helpers
export const { router, procedure } = t;
server/index.ts
Define your API routes here. I have a basic hello
route that takes a string and returns a greeting.
import { z } from "zod";
import { procedure, router } from "@/server/trpc";
export const appRouter = router({
hello: procedure
.input(
z.object({
text: z.string(),
})
)
.query((opts) => {
return {
greeting: `hello ${opts.input.text}`,
};
}),
});
// export type definition of API
export type AppRouter = typeof appRouter;
5. Create tRPC helpers and context provider
utils/trpc.ts
import { createTRPCReact } from "@trpc/react-query";
import type { AppRouter } from "@/server";
function 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}`;
}
export const trpc = createTRPCReact < AppRouter > {};
utils/Provider.tsx
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { httpBatchLink } from "@trpc/client";
import React, { useState } from "react";
import { trpc } from "@/utils/trpc";
function 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}`;
}
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>
);
}
Add the new Provider to your layout so that page.tsx
can make API calls.
import type { Metadata } from "next";
import Provider from "@/utils/Provider";
import { Inter } from "next/font/google";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode,
}) {
return (
<html lang="en">
<body className={inter.className}>
<Provider>{children}</Provider>
</body>
</html>
);
}
6. Create tRPC HTTP handlers
api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { appRouter } from "@/server";
const handler = (req: Request) =>
fetchRequestHandler({
endpoint: "/api/trpc",
req,
router: appRouter,
createContext: () => ({}),
});
export { handler as GET, handler as POST };
7. Make an API call
With tRPC, instead of making a fetch
call, we make a function call to the API.
"use client";
import { trpc } from "@/utils/trpc";
export default function Home() {
const hello = trpc.hello.useQuery({ text: "client" });
return (
<main className="flex min-h-screen flex-col items-center justify-center p-24">
<h1 className="text-4xl font-extrabold">Next.js + tRPC = π</h1>
<section className="mt-10 text-xl text-[#bada55]">
{!hello.data ? <p>Loadingβ¦</p> : <p>{hello.data.greeting}</p>}
</section>
</main>
);
}
Here we are calling our hello
API route with the text, βclientβ. Before we get a response, our page shows the βLoadingβ¦β copy. As soon as we get our response, itβll output the greeting
data. Since our API was written with TypeScript, we will get warnings as soon as we are doing something wrong. For example, if you made this call trpc.hello.useQuery({text: 45})
with an integer, youβll receive a clear error.
Another benefit you get with having your API procedure type safe is that when you hover over our route, hello
, in the function call, you will see a popover with the route properties including the inputs and their types.
One last thing, when you Option-click on the route name, hello
, on the client side, you will jump to the file that contains the procedure definition. This makes it very convenient when you are implementing or working on the API.
Conclusion
tRPC offers a compelling type safe alternative to traditional API communication methods like REST and GraphQL. Its integration with Next.js leverages the full power of TypeScript, providing a seamless development experience with features like auto-completion and immediate type-checking. By following the steps outlined above, you can easily set up tRPC in your Next.js project and start enjoying the benefits of this modern approach to API design and consumption. The end-to-end type safety ensures that both your client and server code are robust and maintainable, making your development process more efficient and error-proof.