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.

src/server/routers/person.ts
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.

src/server/routers/app.ts
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.

src/pages/api/trpc/[trpc].ts
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.

src/utils/trpc.ts
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.

src/pages/_app.tsx
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.

src/pages/index.tsx
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.

Hi! I'm Niall McKenna

Technologist, Maker & Software Developer