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.
Posted ontypescriptwith tags: