January 13, 2023

Xata, TRPC, and Clerk: the Killer Combo!

Fabien Bernard
Fabien BernardSenior TypeScript Engineer

Building an application is not easy, or to be more accurate, this usually starts easy and becomes complicated. Let’s see why. What are the usual challenges we need to deal with?

User Management

Every application has users, and this comes with a lot of specific problems:

  • How do my users log in?
  • How do I store their password? This is quite sensitive.
  • What if I want to connect through my Google or GitHub account?
  • But, if a user did connect with an email and after with Github. Should I link the accounts?
  • How can I ban a user?

Luckily, Clerk solves all those issues for us (with a nice free tier). The only thing we need to care about is storing the userId and making sure a user can only see his own data.

Features Management

One of the main challenges in building an application is to add or remove features. Indeed, without the correct tooling, it’s easy to forget something. Leading to a bug and unhappy customers at the end (or even worse if you are discovering a critical bug on a Sunday).

Again, we can help ourselves using the correct tool or, more precisely, the combination of tools!

Using TypeScript as a base, in combination with Xata and tRPC, we can have full type safety from the database schema to our React components.

For example, if you decide to remove a field from your database, with this setup in place, you can run xata codegen and use TypeScript to track every usage of this field in your entire application, backend side & frontend side.

Of course, this is not 100% bullet-proof guarantee. But trust us: this will keep you on the safe side for most cases.

Getting Started!

To help you in this journey, Xata has a starter with already everything set up for you.

The starter has a simple multi-user Todo application, this should provide you with enough guidance to build a solid stack for your next amazing project!

To get started, let’s start a new project together and see how everything works.

$ npx degit xataio/examples/apps/starter-nextjs-trpc-clerk my-todo-clerk-app
$ cd my-todo-clerk-app
$ npm install # yarn install or pnpm install

So far so good, now let’s connect our application to Clerk and Xata.

Go Xata and create an account. Login and connect the project:

$ npm run xata -- auth login # needed only once$ npm run xata:init

Now create an account and a project at Clerk.

You can then go to Clerk + Next.js integration documentation and copy the credentials to your .env file. That’s it! Just run npm run dev and you have a working application.

Code Walkthrough

// server/trpc.ts
import { initTRPC, TRPCError } from "@trpc/server";
import { Context } from "./context";

const t = initTRPC.context<Context>().create();

const isAuthed = t.middleware(({ next, ctx }) => {
  if (ctx.user === null) {
    throw new TRPCError({ code: "UNAUTHORIZED" });
  }

  return next({
    ctx: {
      user: ctx.user,
    },
  });
});

export const router = t.router;
export const publicProcedure = t.procedure;
export const protectedProcedure = t.procedure.use(isAuthed);

This is where everything starts, we are exposing two procedures, one for public routes and one with auth. All the clerk logic lives in server/context.ts, and the context is injected pages/api/trpc/[trpc].ts.

The idea is quite simple, if Clerk has some information about the user, it’s injected in the context, checking if the user is here, we can’t filter authenticate users. Let’s take todo/list as an example:

// server/routers/todo.ts
import { protectedProcedure, router } from "../trpc";
import { z } from "zod";
import { getXataClient } from "../../utils/xata";
import { TRPCError } from "@trpc/server";

export const todoRouter = router({
  /**
   * List user todo items.
   */
  list: protectedProcedure.query(({ ctx }) => {
    const userId = ctx.user.id; // here `ctx.user` is typed as `User`
    const xata = getXataClient();

    return xata.db.items.filter({ userId }).getAll();
  }),
  /* ... */
});

Here are using protectedProcedure since we need the user information for our database query. The entire function is type-safe, I have access to all user’s data in this function and we are simply using the user.id as a filter. Please note that we don’t have any types annotation as so ever and just returning raw Xata response.

Looking at pages/index.ts, we can look at how to use this route in our application.

https://user-images.githubusercontent.com/1761469/209673800-d4915411-53a4-4b08-9553-31e5e592496d.png

Here, everything is type-safe, serialized through the tRPC layer, and ready to be consumed.

On the React part, tRPC provides you a typed version of react-query, this means you will have all the benefits of React-Query like the query cache but without having to define your own fetcher.

// Function block of `Hello` inside `pages/index.tsx`

const utils = trpc.useContext();

const { mutate: addTodo } = trpc.todo.add.useMutation({
  onMutate: async (newTodo) => {
    await utils.todo.list.cancel();

    const previousTodos = utils.todo.list.getData();

    const optimisticTodo: RouterOutputs["todo"]["list"][-1] = {
      title: newTodo.title,
      createdAt: new Date().toISOString(),
      userId: "me",
      id: "optimistic_" + self.crypto.randomUUID(),
      isCompleted: false,
    };
    utils.todo.list.setData(undefined, (old) =>
      old ? [...old, optimisticTodo] : [optimisticTodo]
    );

    return { previousTodos };
  },
  onError: (_err, _newTodo, context) => {
    utils.todo.list.setData(undefined, context?.previousTodos);
  },
  onSettled: () => {
    utils.todo.list.invalidate();
  },
});

A lot of interesting patterns in this addTodo mutation! First, we are using trpc.useContext(), this is the way to access react-query client with all the types backed! Here, we are implementing a simple optimistic UI: we don’t wait for the backend response to update the UI state!

To do so, we need to create a fake entry in our cache, with the same type as our todo/list return type. We can simply use this RouterOutputs["todo"]["list"][-1] that will infer the type from our router.

Doing so, if tomorrow somebody adds a property to this todo list, this part of the code will not compile until we provide a value for the introduced property, one less bug to fix later!

You also have RouterInputs and ReactQueryOptions as inferred types exported inside utils/trpc.ts. Those helpers are useful to avoid duplicating types. As code, we should always try to avoid duplicate types when we can, so just changing the return type or the parameter of one of our routes will propagate through the entire codebase.

Final Considerations

And this is it for the big lines, the easiest way to learn more about the project is to try to destroy it! Add or remove some code and see how everything plays together.

Fork this example and sign up with Xata today to get started!

Have fun! If you have any suggestions on how to improve this starter or encounter any friction, we are available on our Discord channel

Fabien Bernard

Fabien is a senior TypeScript engineer at Xata who is passionate about type-safety and predictable code. He has been creating solutions to problems around brittle code patterns for years. In his free time, he enjoys climbing, woodworking, and other creative projects.