English

tRPC brings end-to-end type safety to your APIs without code generation.

Setup

npm install @trpc/server @trpc/client @trpc/react-query @trpc/next

Server Setup

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

const t = initTRPC.create();

export const router = t.router;
export const publicProcedure = t.procedure;

// Define your router
export const appRouter = router({
  getUser: publicProcedure
    .input(z.object({ id: z.string() }))
    .query(async ({ input }) => {
      const user = await db.user.findById(input.id);
      return user;
    }),

  createUser: publicProcedure
    .input(
      z.object({
        name: z.string(),
        email: z.string().email(),
      })
    )
    .mutation(async ({ input }) => {
      const user = await db.user.create(input);
      return user;
    }),

  listUsers: publicProcedure.query(async () => {
    return db.user.findMany();
  }),
});

export type AppRouter = typeof appRouter;

Next.js API Handler

// pages/api/trpc/[trpc].ts
import { createNextApiHandler } from '@trpc/server/adapters/next';
import { appRouter } from '@/server/trpc';

export default createNextApiHandler({
  router: appRouter,
  createContext: () => ({}),
});

Client Setup

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

export const trpc = createTRPCNext<AppRouter>({
  config() {
    return {
      links: [
        httpBatchLink({
          url: '/api/trpc',
        }),
      ],
    };
  },
  ssr: false,
});

Usage in Components

import { trpc } from '@/utils/trpc';

function UserList() {
  // Fully typed, with autocompletion!
  const { data, isLoading } = trpc.listUsers.useQuery();

  if (isLoading) return <div>Loading...</div>;

  return (
    <ul>
      {data?.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

function CreateUserForm() {
  const utils = trpc.useContext();
  const mutation = trpc.createUser.useMutation({
    onSuccess: () => {
      // Invalidate and refetch
      utils.listUsers.invalidate();
    },
  });

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);

    mutation.mutate({
      name: formData.get('name') as string,
      email: formData.get('email') as string,
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" required />
      <input name="email" type="email" required />
      <button type="submit" disabled={mutation.isPending}>
        Create User
      </button>
    </form>
  );
}

Authentication Context

import { initTRPC, TRPCError } from '@trpc/server';

interface Context {
  user?: {
    id: string;
    role: string;
  };
}

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

// Protected procedure
const protectedProcedure = t.procedure.use(({ ctx, next }) => {
  if (!ctx.user) {
    throw new TRPCError({ code: 'UNAUTHORIZED' });
  }
  return next({
    ctx: {
      user: ctx.user,
    },
  });
});

export const appRouter = t.router({
  getProfile: protectedProcedure.query(({ ctx }) => {
    // ctx.user is guaranteed to exist
    return db.user.findById(ctx.user.id);
  }),
});

Subscriptions (WebSocket)

import { observable } from '@trpc/server/observable';

export const appRouter = router({
  onUserUpdate: publicProcedure
    .input(z.object({ userId: z.string() }))
    .subscription(({ input }) => {
      return observable<User>((emit) => {
        const onUpdate = (user: User) => {
          if (user.id === input.userId) {
            emit.next(user);
          }
        };

        eventEmitter.on('userUpdate', onUpdate);

        return () => {
          eventEmitter.off('userUpdate', onUpdate);
        };
      });
    }),
});

// Client usage
function UserProfile({ userId }: { userId: string }) {
  trpc.onUserUpdate.useSubscription(
    { userId },
    {
      onData: (user) => {
        console.log('User updated:', user);
      },
    }
  );
}

Batching and Caching

export const trpc = createTRPCNext<AppRouter>({
  config() {
    return {
      links: [
        httpBatchLink({
          url: '/api/trpc',
          maxURLLength: 2083,
        }),
      ],
      queryClientConfig: {
        defaultOptions: {
          queries: {
            staleTime: 60 * 1000,
          },
        },
      },
    };
  },
});

tRPC eliminates API contracts while providing full type safety across your stack.

0
0
0
0