tRPC

Shipkit includes a tRPC setup for type-safe API calls between client and server. It's wired up as a demo but ready for production use.

Structure

src/lib/trpc/
├── api/
│   ├── root.ts          # App router (registers all sub-routers)
│   ├── trpc.ts          # Context, middleware, procedure helpers
│   └── routers/
│       └── post.ts      # Example router
├── react.tsx            # React Query + tRPC client provider
├── query-client.ts      # Shared QueryClient config
└── server.ts            # Server-side caller

Adding a Router

  1. Create a file in src/lib/trpc/api/routers/:
import { z } from "zod";
import { createTRPCRouter, publicProcedure, protectedProcedure } from "@/lib/trpc/api/trpc";

export const myRouter = createTRPCRouter({
  list: publicProcedure.query(async ({ ctx }) => {
    return ctx.db.query.myTable.findMany();
  }),

  create: protectedProcedure
    .input(z.object({ name: z.string() }))
    .mutation(async ({ ctx, input }) => {
      // ctx.session.user is guaranteed here
      await ctx.db.insert(myTable).values({ name: input.name });
    }),
});
  1. Register it in src/lib/trpc/api/root.ts:
import { myRouter } from "./routers/my-router";

export const appRouter = createTRPCRouter({
  post: postRouter,
  my: myRouter,
});

Client Usage

"use client";
import { api } from "@/lib/trpc/react";

export function MyComponent() {
  const { data } = api.my.list.useQuery();
  const create = api.my.create.useMutation();

  return <button onClick={() => create.mutate({ name: "test" })}>Create</button>;
}

Server Usage

import { createCaller } from "@/lib/trpc/api/root";

const trpc = createCaller(createContext);
const items = await trpc.my.list();

Procedures

TypeImportAuth required
publicProcedure@/lib/trpc/api/trpcNo
protectedProcedure@/lib/trpc/api/trpcYes — ctx.session guaranteed

Demo Route

The tRPC demo page lives at /trpc (src/app/(app)/(demo)/trpc/page.tsx).