Build a fully typed application with Clerk for authentication and tRPC for a robust stack.
Written by
Fabien Bernard
Published on
January 13, 2023
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?
Every application has users, and this comes with a lot of specific problems:
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.
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.
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.
// 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.
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.
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