End-to-end typesafe APIs for TypeScript monorepos. Eliminates the need for code generation or a schema — your TypeScript types ARE your API contract. Pairs perfectly with Next.js.
Full-stack TypeScript monorepo (Next.js, SvelteKit)
You want autocomplete on API calls without a separate schema step
Small-to-medium team that wants to move fast without REST boilerplate
What drives the Trust Score
Last 12 months
Public API consumed by non-TypeScript clients
Large polyglot team — REST/GraphQL are language-agnostic
Microservices that communicate across repos
Free tier & paid plans
Open source, free forever
N/A
MIT license
Other options worth considering
Complementary tools that pair well with tRPC
Docs, videos, tutorials, and courses
Repository and installation options
View on GitHub
github.com/trpc/trpc
npm install @trpc/server @trpc/client @trpc/react-queryCopy and adapt to get going fast
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
const t = initTRPC.create();
export const appRouter = t.router({
getUser: t.procedure
.input(z.object({ id: z.string() }))
.query(async ({ input }) => {
return db.users.findUnique({ where: { id: input.id } });
}),
createPost: t.procedure
.input(z.object({ title: z.string(), body: z.string() }))
.mutation(async ({ input }) => {
return db.posts.create({ data: input });
}),
});
export type AppRouter = typeof appRouter;Common usage patterns
Protected procedure
Add authentication middleware to a tRPC procedure
// server/trpc.ts
import { initTRPC, TRPCError } from '@trpc/server';
import { getServerSession } from 'next-auth';
const t = initTRPC.context<{ session: Session | null }>().create();
const isAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.session?.user) throw new TRPCError({ code: 'UNAUTHORIZED' });
return next({ ctx: { session: ctx.session } });
});
export const protectedProcedure = t.procedure.use(isAuthed);Infinite scroll with tRPC
Paginated query with cursor-based pagination
// server/router.ts
import { z } from 'zod';
export const postRouter = router({
list: publicProcedure
.input(z.object({ cursor: z.string().optional(), limit: z.number().default(10) }))
.query(async ({ input }) => {
const posts = await db.posts.findMany({
take: input.limit + 1,
cursor: input.cursor ? { id: input.cursor } : undefined,
orderBy: { createdAt: 'desc' },
});
const nextCursor = posts.length > input.limit ? posts.pop()!.id : undefined;
return { posts, nextCursor };
}),
});Real experiences from developers who've used this tool