Build a full stack Typescript app with Next.js and tRPC
last updated: July 30, 2022
tRPC is a library that helps us build type safe APIs in Typescript based projects.
One of the main advantages of tRPC is that it allows us to create an API in a full stack application that makes it easy to keep the backend and frontend type safe.
From my initial experimentation with it, tRPC feels like a cross between a standard REST api and a GraphQL.
Getting started
In this blog post we will build a simple full stack application using tRPC and Next.js.
Lets first create a new Next.js project and install tRPC.
npx create-next-app@latest --ts
npm install @trpc/client @trpc/server @trpc/react @trpc/next zod react-query@3
We will use the following file structure in our project
├── src
│ ├── pages
│ │ ├── _app.tsx # <-- add `withTRPC()`-HOC here
│ │ ├── api
│ │ │ └── trpc
│ │ │ └── [trpc].ts # <-- tRPC HTTP handler
│ │ └── index.tsx
│ ├── server
│ │ ├── routers
│ │ │ ├── app.ts # <-- main app router
│ │ │ ├── person.ts # <-- sub routers
│ │ │ └── [..]
│ │ └── createRouter.ts # <-- router helper
│ └── utils
│ └── trpc.ts # <-- your typesafe tRPC hooks
Create a tRPC Router
We will now create a tRPC Router, which includes several subrouters that will be used to create our API. You can think of each subrouter as being a difference resource in REST apis.
Our person
subrouter will be used to get a list of people, we will hard code the response in this example.
import * as trpc from '@trpc/server';
export type Person = {
id: string;
firstName: string;
lastName: string;
dateOfBirth: string;
email?: string;
};
export const personRouter = trpc.router().query("getAll", {
resolve() {
// In an actual application we would get this data from a database.
const people: Person[] = [
{ id: "1", firstName: "John", lastName: "Smith", dateOfBirth: "2000-06-12" },
{ id: "2", firstName: "Jane", lastName: "Doe", dateOfBirth: "1997-01-01" },
];
return { people }
}
})
Next we will create our main router, which will include our person router.
import * as trpc from '@trpc/server';
import { personRouter } from './person';
export const appRouter = createRouter().merge('person.', personRouter) // this is where we can merge our subrouters together
export type AppRouter = typeof appRouter;
and then we create point our Next.js api page to our appRouter.
import * as trpcNext from '@trpc/server/adapters/next';
import { appRouter } from '../../../server/routers/app';
// export API handler
export default trpcNext.createNextApiHandler({
router: appRouter,
createContext: () => null,
});
Create tRPC hooks
We will then create a set of hooks we can use to interact with our newly created API, we will use the createReactQueryHooks
function which give us all the beneifts of react-query
.
import { createReactQueryHooks } from '@trpc/react';
import { AppRouter } from '../server/routers/app';
export const trpc = createReactQueryHooks<AppRouter>();
Wrap our _app.tsx
in a HOC
The createReactQueryHooks
client function is expecting certain parameters to be passed into it via the Context API, we can do this by wrapping our app in a withTRPC
higher order component.
import "../styles/globals.css";
import type { AppProps } from "next/app";
import { withTRPC } from "@trpc/next";
import { AppRouter } from "../server/routers/app";
function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />;
}
export default withTRPC<AppRouter>({
config({}) {
/**
* If you want to use SSR, you need to use the server's full URL
* @link https://trpc.io/docs/ssr
*/
const url = "http://localhost:3000/api/trpc";
return {
url,
/**
* @link https://react-query.tanstack.com/reference/QueryClient
*/
// queryClientConfig: { defaultOptions: { queries: { staleTime: 60 } } },
};
},
/**
* @link https://trpc.io/docs/ssr
*/
ssr: true,
})(MyApp);
Let the magic begin
Now we have set up our application, we can start making API requests.
import type { NextPage } from "next";
import styles from "../styles/Home.module.css";
import { trpc } from "../utils/trpc";
const Home: NextPage = () => {
// We have access to all of the React Query hook functions
const { isLoading, isError, data, error, refetch } = trpc.useQuery([
"person.getAll",
]);
// our data.people return is fully typed, and we can now use it within our component
return (
<div className={styles.container}>
{data && (
<div>
{data.people.map((person) => {
return (
<p>
{person.firstName} {person.lastName}
</p>
);
})}
</div>
)}
</div>
);
};
export default Home;
The above component may not be particularly special, but the call to get all people is now fully typed and if we decide to add to our api the client will immediatly be able to infer the types.
Summary
We now have a basic tRPC api setup in a Next.js app, from here it is easy to build out more subrouters and our client side code can immediatly make use of the types that are inferred from them.
Closing thoughts
Hopefully this post has been helpful to demonstrate how tRPC can be quite useful in certain situations.
It seems tRPC is great at providing a good developer experience as we can rely on the strong type system it gives us, Although it does rely on the backend and frontend code being part of the same project.
I can see it being useful in smaller projects where there is only a single client for our api. The tRPC library itself also relys heavily on inferred types, which can make Typescript compiling slow on a larger project that has a lot of inferred typing.
I'll be keeping an eye on this library as it continues to grow, there are already some interesting architectural changes in v10 that is still in development as of this blog post.