Build end-to-end typesafe APIs with tRPC
Tags: typescript, nextjs, webdev, api
Published: 2022-05-21 14:06:27

[tRPC](https://trpc.io) is a tool that provides type-safety between your front and back-ends, hence it makes it *really* easy to build scalable and robust backends quickly for your Next.js and Node apps. In this article we will be looking at what tRPC is, and how to set it up and use it with Next.js.

## What is tRPC?
tRPC is a very light library which lets you build fully typesafe APIs without the need of schemas or code generation. It allows sharing of types between the client and server and just imports the types and not the actual server code, so none of the server code is exposed in the frontend. With end-to-end type-safety, you're able to catch errors between your frontend and backend at compile and build time.

Currently, the dominant way of making typesafe APIs is using [GraphQL](https://graphql.org/) (it's awesome). Since GraphQL is a query language for APIs, it doesn't take full advantage of TypeScript, it comes with excess boilerplate code and requires a lot of initial setup.

If you're already using TypeScript everywhere, you can share types directly between the client and the server without the need of any code generation.

## Installing tRPC in Next.js
Lets first create a Next.js project with TypeScript.

```bash
npx create-next-app next-with-trpc --ts
```

Let's now use the recommended folder structure by tRPC. Open the project in your favourite editor, make a `src` directory and move the `pages` and `styles` directory inside it. Your folder structure should look something like this.

```
.
├── src
│   ├── pages
│   │   ├── _app.tsx
│   │   ├── api
│   │   │   
│   │   └── [..]
└── [..]
```

Once this is done, let's install tRPC.

```
npm i @trpc/client @trpc/server @trpc/react @trpc/next zod react-query
```

tRPC is a built on top of [react-query](https://react-query.tanstack.com/), which is a package for fetching, caching, and updating data without the need of any global state. We are also using [zod](https://zod.dev/) for schema and input validations. You can also use other validation libraries like [yup](https://github.com/jquense/yup), [Superstruct](https://github.com/ianstormtaylor/superstruct), etc. [Read more](https://trpc.io/docs/router).

Also make sure that in your `tsconfig.json`, strict mode is enabled.

```json
// tsconfig.json
{
  // ...
  "compilerOptions": {
    // ...
    "strict": true
  }
}
```

This is so that zod can run correctly, and in general, having strict mode enabled is just good.

## Creating our server

Our tRPC server will be deployed as a [Next.js API route](https://nextjs.org/docs/api-routes/introduction). This code only runs on the server so it doesn't affect the bundle sizes in any way.

### Creating our context

Let's first create a directory inside `src` called `server`. In here, we need to first create a context. Create a file called `context.ts` and add the following code to it.

```ts
// src/server/context.ts

import { CreateNextContextOptions } from "@trpc/server/adapters/next";
import { inferAsyncReturnType } from "@trpc/server";

export async function createContext(ctxOptions?: CreateNextContextOptions) {
  const req = ctxOptions?.req;
  const res = ctxOptions?.res;

  return {
    req,
    res,
  };
}

export type MyContextType = inferAsyncReturnType<typeof createContext>;
```

This will be available as `ctx` in all our resolvers which we'll write in a bit. Right now, we're just passing the request and response to our routes, but you can add other things like JWT tokens, cookies or even Prisma Client code.

### Creating our router

Now let's create a file called `createRouter.ts` in the `server` directory. We'll be setting up a simple router. Copy the following code to it.

```ts
// src/server/createRouter.ts

import * as trpc from "@trpc/server";

import type { MyContextType } from "./context";

export function createRouter() {
  return trpc.router<MyContextType>();
}
```

### Creating our routes

Let's create a new directory inside `src/server` called `routers`. Make a file called `app.ts` inside it. This will be the root route.

```ts
// src/server/routers/app.ts

import { createRouter } from "../createRouter";

export const appRouter = createRouter();

export type AppRouter = typeof appRouter;
```

Now let's create a router that takes a name as input and returns it to the client. Add a file called `name.ts`.

```ts
// src/server/routers/name.ts

import { z } from "zod";

import { createRouter } from "../createRouter";

export const nameRouter = createRouter().query("getName", {
  input: z
    .object({
      name: z.string().nullish(),
    })
    .nullish(),
  resolve({ input }) {
    return { greeting: `Hello ${input?.name ?? "world"}!` };
  },
});
```

Just like GraphQL, tRPC uses queries and mutations. A `query` is used for fetching data and `mutations` are used to create, update, and delete data. Here we are creating a query to get a name. The name of our query is `getName`. Here, `input` takes the user input which is validated using zod. When this endpoint is requested, the `resolve` function is called and it returns the *hello world* greeting. because why not. 


Now let's merge this route in our root route. Come back to `app.ts` and add the following code.

```ts
// src/server/routers/app.ts

import { createRouter } from "../createRouter";
import { nameRouter } from "./name";

export const appRouter = createRouter().merge("names.", nameRouter);

export type AppRouter = typeof appRouter;
```

That `.` at the end of `names.` is intentional for reasons you'll see soon.

### Creating our Next.js API route

Let's create a `trpc` directory inside `src/pages/api`. Inside it create a file called `[trpc].ts`. Just a reminder of our folder structure.

```
.
├── src
│   ├── pages
│   │   ├── _app.tsx 
│   │   ├── api
│   │   │   └── trpc
│   │   │       └── [trpc].ts 
│   │   └── [..]
│   ├── server
│   │   ├── routers
│   │   │   ├── app.ts   
│   │   │   ├── name.ts  
│   │   │   └── [..]
│   │   ├── context.ts      
│   │   └── createRouter.ts 
└── [..]
```

Here we will implement our tRPC router. As I had said before, our server will be deployed as a Next.js API route.

```ts
// src/pages/api/trpc/[trpc].ts

import { createNextApiHandler } from "@trpc/server/adapters/next";

import { appRouter } from "../../../server/routers/app";
import { createContext } from "../../../server/context";

export default createNextApiHandler({
  router: appRouter,
  createContext,
  batching: {
    enabled: true,
  },
  onError({ error }) {
    if (error.code === "INTERNAL_SERVER_ERROR") {
      console.error("Something went wrong", error);
    }
  },
});
```

We're passing it our router, our `createConext` function, enabling batching and logging errors.

With that we're pretty much done with the backend. Let's now work on our frontend.

### Calling our tRPC API routes

Let's now connect our backend with our frontend. Go to `src/pages/_app.tsx`. Here we are going to configure tRPC and React Query. Copy the following code.

```tsx
// src/pages/_app.ts

import { AppType } from "next/dist/shared/lib/utils";
import { withTRPC } from "@trpc/next";

import { AppRouter } from "./api/trpc/[trpc]";
import "../styles/globals.css";

const MyApp: AppType = ({ Component, pageProps }) => {
  return <Component {...pageProps} />;
};

const getBaseUrl = () => {
  if (process.browser) return "";
  if (process.env.NEXT_PUBLIC_VERCEL_URL)
    return `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`;

  return `http://localhost:${process.env.PORT ?? 3000}`;
};

export default withTRPC<AppRouter>({
  config({ ctx }) {
    const url = `${getBaseUrl()}/api/trpc`;

    return {
      url,
    };
  },
  ssr: false,
})(MyApp);
```

We're also setting `ssr` (Server Side Rendering) to be false for now.

Next, create a `utils` directory inside `src`. Inside `utils`, create a file called `trpc.ts`. Folder structure for reference:

```
.
├── src
│   ├── pages
│   │   ├── _app.tsx 
│   │   ├── api
│   │   │   └── trpc
│   │   │       └── [trpc].ts 
│   │   └── [..]
│   ├── server
│   │   ├── routers
│   │   │   ├── app.ts   
│   │   │   ├── name.ts  
│   │   │   └── [..]
│   │   ├── context.ts      
│   │   └── createRouter.ts 
|   └── utils
│       └── trpc.ts
└── [..]
```

Here we're going to create a hook to use tRPC on the client. 

```ts
// src/utils/trpc.ts

import { createReactQueryHooks } from "@trpc/react";
import { inferProcedureOutput } from "@trpc/server";

import { AppRouter } from "../server/routers/app";

export const trpc = createReactQueryHooks<AppRouter>();

export type inferQueryOutput<
  TRouteKey extends keyof AppRouter["_def"]["queries"]
> = inferProcedureOutput<AppRouter["_def"]["queries"][TRouteKey]>;
```

This hook is strongly typed using our API type signature. This is what enables the end-to-end typesafety in our code. For example, if we change a router's name in the backend, it will show an error in the frontend where we're calling the route. It allows us to call our backend and get fully typed inputs and outputs from it. It also gives us wonderful autocomplete.

Let's now actually use our query that we defined earlier. Go to the index page and instead of copy pasting this code block, type it yourself to experience the **magic** of tRPC. The autocomplete is just crazy.

```ts
// src/pages/index.tsx

import { trpc } from "../utils/trpc";

export default function Name() {
  const nameQuery = trpc.useQuery(["names.getName", { name: "nexxel" }]);

  return (
    <>
      {nameQuery.data ? (
        <h1>{nameQuery.data.greeting}</h1>
      ) : (
        <span>Loading...</span>
      )}
    </>
  );
}
```

Did you see the magic? In case you're lazy and just copied the code, I got you covered ;) Look at this 😱.

![autocomplete](https://us-east-1.tixte.net/uploads/nexxel.needs.rest/Code_XJsLtO8b8P.gif)

![autocomplete](https://us-east-1.tixte.net/uploads/nexxel.needs.rest/Code_3aJUBQVDTI.gif)

The autocomplete is soo good. And it catches any kind of type errors you make without the need of declaring any types or interfaces manually. It's crazy good.

Now, start the dev server and see the greeting on the screen!

![loading](https://us-east-1.tixte.net/uploads/nexxel.needs.rest/brave_HKwxL4t2a7.gif)

Notice the loading state. This is because we set `ssr` to false when we configured tRPC in `_app.tsx`. So it's being client side rendered. Go to `src/pages/_app.tsx` and set `ssr` to true now and see what happens.

![not loading](https://us-east-1.tixte.net/uploads/nexxel.needs.rest/brave_eosogl59Pz.gif)

Now there is no loading state because the data is being fetched server side and then it is being rendered. You can use whatever suits your needs.

## Conclusion

In this article, we looked at what tRPC is and how to use it with a Next.JS app. tRPC makes building typesafe APIs incredibly easy and improves the DX by a million times. You can use it with not only Next.js but also React, Node and there's adapters being developed for Vue and Svelte as well. I recommend using it for pretty much any project you make now. For more information checkout the [tRPC docs](https://trpc.io).

Code: https://github.com/nexxeln/trpc-nextjs

Thank you for reading.