Full-stack, type-safe applications development with Next.js and tRPC
Tags: javascript, react, webdev, typescript
Published: 2022-10-11 07:59:46

## Table Of Contents
* [Introduction](#introduction)
* [tRPC prerequisites](#trpc-prerequisites)
* [tRPC project bootstrap](#trpc-project-bootstrap)
* [tRPC backend development](#trpc-backend-development)
* [UI development](#ui-development)

### Introduction <a name="introduction"></a>
When working with a modern UI library, we often need to fetch data from an external source, a REST or GraphQL API. Synchronizing the client with a server state comes with some challenges, including type definitions and safety. All the external data sources define schemas, but they are not immediately consumable from our UI without redefining them, leveraging types generations libraries, or importing monorepo packages. The lack of immediately available types and the introduction of more dedicated tools create friction and slow developers’ productivity and velocity. Suppose your organization is also split into a front-end and back-end team. The communication overhead adds a layer of potential conflicts because the two parts must agree on the schema before starting the development of features.

tRPC provides all the tools to create full-stack end-to-end type-safe applications. It is a library that combines the power of TypeScript and react-query to register all the server endpoints and make them instantly available with types to the client. It increases team productivity because backend endpoints are just functions, and the developers can effectively work on the entire stack. Any team can significantly benefit from this tool because code generation, types package, and communication overhead are phased out.

tRPC also has some downsides. A single developer maintains it. Files cannot be uploaded without any 3rd parties services like Amazon S3. WebSockets have limited support, and data is limited to JSON.

**Pros**
- Full-stack end-to-end type-safe application development.
- Better team productivity and velocity.
- Minimal communication overhead.
- If you trust the type system and there is no complex business logic, unit tests may not be required.

**Cons**
- Solo maintainer.
- TypeScript could slow down your IDE.
- Data is limited to JSON.
- Limited support for WebSocket.

### tRPC Tutorial

#### tRPC Prerequisites <a name="trpc-prerequisites"></a>

The tutorial assumes you are familiar with the following libraries:
- React,
- Next.js, 
- Prisma or other ORMs,
- react-query.

#### tRPC project bootstrap <a name="trpc-project-bootstrap"></a>

Let’s initialize a new tRPC project with the `create-t3-app` CLI:

```npx create-t3-app@latest```

You will be prompted to answer some questions:
- Name your application.
- Select TypeScript.
- Select Prisma and tRPC.
- Initialize a git repository.
- Run `npm install`.

![T3 questions](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/grwjnt6yismokmn3w1s1.png)

Once the project is bootstrapped, navigate to `prisma/schema.prisma` and update it with a new `Blog` model.

```prisma
model Blog {
    id        String   @id @default(cuid())
    title     String
    content   String
    createdAt DateTime @default(now())
}
```

From your terminal, run `npx prisma db push` to apply the model to a local SQLite database.

#### tRPC Backend development <a name="trpc-project-bootstrap"></a>

We are ready to work on the backend of our application. Navigate to `src/server/router` and create a new file named `blog.ts.`  Please copy the following code to create two endpoints. One handles the creation of a blog post, and the other lists the inserted articles.

```typescript
import { createRouter } from "./context";
// `zod` is a schema declaration and validation library. We use it to define the shape of request arguments.
import { z } from "zod";

// `createRouter` exposes functions to create new API endpoints.
export const blogRouter = createRouter()
// A `mutation` defines an operation that modifies our data.
// It behaves like`POST`, `PUT`, `DELETE` operations.
// The input property accepts an optional `zod` schema to validate the request body.
  .mutation("create", {
    input: z.object({
      title: z.string(),
      content: z.string(),
    }),
// The API handler is a simple `resolve` function that contains all the business logic to handle the request.
    resolve({ input, ctx }) {
      return ctx.prisma.blog.create({
        data: {
          title: input.title,
          content: input.content,
        },
      });
    },
  })
// A `query` defines an operation that reads data from our backend.
// It behaves like a GET request.
  .query("all", {
    resolve({ ctx }) {
      return ctx.prisma.blog.findMany();
    },
  });
```


Once we have defined the routes, we can register them in `src/server/router/index.ts.`

```typescript
// src/server/router/index.ts
import { createRouter } from "./context";
import superjson from "superjson";

import { blogRouter } from "./blog";

export const appRouter = createRouter()
  .transformer(superjson)
// Registering the blogRouter
  .merge("blog.", blogRouter);

// export type definition of the API
export type AppRouter = typeof appRouter;
```

#### UI development <a name="ui-development"></a>

Navigate to `src/pages/index.tsx` and paste the following code. The tRPC utilities that wrap react-query enable us to access the defined type-safe backend procedures.

```typescript
import type { NextPage } from "next";
import { useState } from "react";
import BlogPost from "../components/BlogPost";
import Layout from "../components/Layout";

// import trpc utils that wrap react-query
import { trpc } from "../utils/trpc";

const Home: NextPage = () => {
// Hovering on data shows the types defined in our backend
  const { isLoading, data } = trpc.useQuery(["blog.all"]);

  return (
    <Layout>
      <h1>My personal blog</h1>
      {isLoading ? (
        <div>Loading posts</div>
      ) : (
        data?.map((blog) => <BlogPost blog={blog} key={blog.id} />)
      )}
      <CreatePostSection />
    </Layout>
  );
};

export default Home;

const CreatePostSection = () => {
  const [title, setTitle] = useState("");
  const [content, setContent] = useState("");
  const ctx = trpc.useContext();
  const createMutation = trpc.useMutation("blog.create");

  return (
    <div>
      {createMutation.isLoading}
      <h2>Create a new blog post</h2>
      <div>
        <input
          name="title"
          placeholder="Your title..."
          value={title}
          onChange={(e) => setTitle(e.target.value)}
        />
      </div>
      <div>
        <textarea
          rows={10}
          cols={50}
          value={content}
          placeholder="Start typing..."
          onChange={(e) => setContent(e.target.value)}
        />
      </div>
      <div>
        <button
          onClick={() =>
// mutate exposes the types of arguments as defined in our router
            createMutation.mutate(
              { title, content },
              {
                onSuccess() {
                  ctx.invalidateQueries("blog.all");
                  setContent("");
                  setTitle("");
                },
              }
            )
          }
          disabled={createMutation.isLoading}
        >
          Create new post
        </button>
      </div>
    </div>
  );
};
```

Run `npm run dev` to check the platform. You can further practice with tRPC by expanding the application with new features. Some examples are finding by id, deleting, or updating a single post. A complete solution can be found here https://github.com/andrew-hu368/t3-blog.