📏 Rules
🔵 Official

trpc

## Overview [tRPC](https://trpc.io/) enables end-to-end typesafe APIs, allowing you to build and consume APIs without schemas, code generation, or runtime errors. These rules will help you follow bes

❤️ 0
⬇️ 0
👁 2
Share

Description

Overview

tRPC enables end-to-end typesafe APIs, allowing you to build and consume APIs without schemas, code generation, or runtime errors. These rules will help you follow best practices for tRPC v11.

Project Structure

For a clean tRPC setup, follow this recommended structure:

.
├── src
│   ├── pages
│   │   ├── _app.tsx  # add `createTRPCNext` setup here
│   │   ├── api
│   │   │   └── trpc
│   │   │       └── [trpc].ts  # tRPC HTTP handler
│   │   ├── server
│   │   │   ├── routers
│   │   │   │   ├── _app.ts  # main app router
│   │   │   │   ├── [feature].ts  # feature-specific routers
│   │   │   │   └── [...]
│   │   │   ├── context.ts   # create app context
│   │   │   └── trpc.ts      # procedure helpers
│   │   └── utils
│   │       └── trpc.ts  # typesafe tRPC hooks

Server-Side Setup

Initialize tRPC Backend

// server/trpc.ts
import { initTRPC } from '@trpc/server';

// Initialize tRPC backend (should be done once per backend)
const t = initTRPC.create();

// Export reusable router and procedure helpers
export const router = t.router;
export const publicProcedure = t.procedure;

Create Router

// server/routers/_app.ts
import { z } from 'zod';
import { router, publicProcedure } from '../trpc';

export const appRouter = router({
  // Your procedures here
  greeting: publicProcedure
    .input(z.object({ name: z.string() }))
    .query(({ input }) => {
      return `Hello ${input.name}`;
    }),
});

// Export type definition of API (not the router itself!)
export type AppRouter = typeof appRouter;

Client-Side Setup

Next.js Integration

// utils/trpc.ts
import { httpBatchLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import type { AppRouter } from '../server/routers/_app';

function getBaseUrl() {
  if (typeof window !== 'undefined') return ''; // browser should use relative path
  if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; // SSR should use vercel url
  return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost
}

export const trpc = createTRPCNext<AppRouter>({
  config() {
    return {
      links: [
        httpBatchLink({
          url: `${getBaseUrl()}/api/trpc`,
          // Include auth headers when needed
          async headers() {
            return {
              // authorization: getAuthCookie(),
            };
          },
        }),
      ],
    };
  },
  ssr: false, // Set to true if you want to use server-side rendering
});

Best Practices

  1. Use Zod for Input Validation: Always validate procedure inputs with Zod for better type safety and runtime validation.

    import { z } from 'zod';
    
    procedure
      .input(z.object({ 
        id: z.string().uuid(),
        email: z.string().email(),
        age: z.number().min(18) 
      }))
      .mutation(({ input }) => { /* your code */ })
    
  2. Organize Routers by Feature: Split your routers into logical domains/features rather than having one large router.

    // server/routers/user.ts
    export const userRouter = router({
      list: publicProcedure.query(() => { /* ... */ }),
      byId: publicProcedure.input(z.string()).query(({ input }) => { /* ... */ }),
      create: publicProcedure.input(/* ... */).mutation(({ input }) => { /* ... */ }),
    });
    
    // server/routers/_app.ts
    import { userRouter } from './user';
    import { postRouter } from './post';
    
    export const appRouter = router({
      user: userRouter,
      post: postRouter,
    });
    
  3. Use Middleware for Common Logic: Apply middleware for authentication, logging, or other cross-cutting concerns.

    const isAuthed = t.middleware(({ next, ctx }) => {
      if (!ctx.user) {
        throw new TRPCError({ code: 'UNAUTHORIZED' });
      }
      return next({
        ctx: {
          // Add user information to context
          user: ctx.user,
        },
      });
    });
    
    const protectedProcedure = t.procedure.use(isAuthed);
    
  4. Use Proper Error Handling: Utilize tRPC's error handling for consistent error responses.

    import { TRPCError } from '@trpc/server';
    
    publicProcedure
      .input(z.string())
      .query(({ input }) => {
        const user = getUserById(input);
        if (!user) {
          throw new TRPCError({
            code: 'NOT_FOUND',
            message: `User with id ${input} not found`,
          });
        }
        return user;
      });
    
  5. Use Data Transformers: Use SuperJSON for automatic handling of dates, Maps, Sets, etc.

    import { initTRPC } from '@trpc/server';
    import superjson from 'superjson';
    
    const t = initTRPC.create({
      transformer: superjson,
    });
    
  6. Leverage React Query Integration: For React projects, use tRPC's React Query utilities for data fetching, mutations, and caching.

    function UserProfile({ userId }: { userId: string }) {
      const { data, isLoading, error } = trpc.user.byId.useQuery(userId);
    
      if (isLoading) return <div>Loading...</div>;
      if (error) return <div>Error: {error.message}</div>;
    
      return <div>{data.name}</div>;
    }
    
  7. Context Creation: Create a proper context object to share resources across procedures.

    // server/context.ts
    import { inferAsyncReturnType } from '@trpc/server';
    import * as trpcNext from '@trpc/server/adapters/next';
    import { prisma } from './prisma';
    
    export async function createContext({
      req,
      res,
    }: trpcNext.CreateNextContextOptions) {
      const user = await getUser(req);
      return {
        req,
        res,
        prisma,
        user,
      };
    }
    
    export type Context = inferAsyncReturnType<typeof createContext>;
    
  8. Type Exports: Only export types, not the actual router implementations, from your server code to client code.

    // Export type router type signature, NOT the router itself
    export type AppRouter = typeof appRouter;
    
  9. Procedure Types: Use different procedure types for different authorization levels.

    export const publicProcedure = t.procedure;
    export const protectedProcedure = t.procedure.use(isAuthed);
    export const adminProcedure = t.procedure.use(isAdmin);
    
  10. Performance Optimization: Use batching and prefetching for optimized data loading.

    // Client-side batching in Next.js setup
    httpBatchLink({
      url: `${getBaseUrl()}/api/trpc`,
      maxURLLength: 2083,
    })
    
    // Prefetching data in Next.js
    export async function getStaticProps() {
      const ssg = createServerSideHelpers({
        router: appRouter,
        ctx: {},
      });
    
      await ssg.post.byId.prefetch('1');
    
      return {
        props: {
          trpcState: ssg.dehydrate(),
        },
        revalidate: 1,
      };
    }
    

Version Compatibility

This guide is for tRPC v11, which requires:

  • TypeScript >= 5.7.2
  • Strict TypeScript mode ("strict": true in tsconfig.json)

Further Resources

Reviews (0)

Sign in to write a review.

No reviews yet. Be the first to review!

Comments (0)

Sign in to join the discussion.

No comments yet. Be the first to share your thoughts!

Compatible Platforms

Pricing

Free

Related Configs