Next.js 15 + tRPC + Prisma: The Winning Trio for Your SaaS
Step-by-step setup of the most productive technical trio for a SaaS in 2026: App Router, end-to-end type safety, and a modern ORM.
The problem: REST is dead (for SaaS)
When you build a SaaS, you spend a huge amount of time keeping your API and your frontend in sync. Duplicated types, duplicated validation, documentation that drifts out of date... it's all wasted time.
tRPC removes this problem by sharing types between server and client automatically.
Setup: from zero to productive
1. Project structure
src/
server/
api/
root.ts ← Router registry
trpc.ts ← Procedures and middlewares
routers/
invoice.ts ← Invoicing router
client.ts ← Clients router
...
app/
dashboard/
facturation/
page.tsx ← React page (Server Component)
2. Define the procedures
Procedures are the heart of tRPC. They replace REST endpoints:
// src/server/api/trpc.ts
import { initTRPC, TRPCError } from "@trpc/server";
const t = initTRPC.context<Context>().create();
// Public procedure: accessible without auth
export const publicProcedure = t.procedure;
// Protected procedure: authentication required
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.session?.user) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return next({ ctx: { session: ctx.session } });
});
// Staff procedure: role ≠ CLIENT
export const staffProcedure = protectedProcedure.use(({ ctx, next }) => {
if (ctx.session.user.role === "CLIENT") {
throw new TRPCError({ code: "FORBIDDEN" });
}
return next({ ctx });
});3. Build a router
// src/server/api/routers/invoice.ts
import { z } from "zod";
import { createTRPCRouter, staffProcedure } from "~/server/api/trpc";
export const invoiceRouter = createTRPCRouter({
getAll: staffProcedure
.input(
z.object({
status: z.enum(["DRAFT", "SENT", "PAID"]).optional(),
}),
)
.query(async ({ ctx, input }) => {
return ctx.orgDb.invoice.findMany({
where: input.status ? { status: input.status } : undefined,
orderBy: { createdAt: "desc" },
include: { client: { select: { name: true } } },
});
}),
create: staffProcedure
.input(
z.object({
clientId: z.string(),
lines: z.array(
z.object({
description: z.string(),
quantity: z.number().positive(),
unitPrice: z.number().positive(),
}),
),
}),
)
.mutation(async ({ ctx, input }) => {
// ctx.orgDb auto-filters by organizationId
return ctx.orgDb.invoice.create({
data: {
clientId: input.clientId,
lines: { create: input.lines },
authorId: ctx.session.user.id,
},
});
}),
});4. Call it from the frontend
"use client";
import { api } from "~/trpc/react";
export function InvoiceList() {
const { data, isLoading } = api.invoice.getAll.useQuery({
status: "DRAFT",
});
if (isLoading) return <Skeleton />;
return (
<ul>
{data?.map((invoice) => (
<li key={invoice.id}>
{invoice.client.name} · €{invoice.totalHT}
</li>
))}
</ul>
);
}Notice: zero type annotations on the client side. TypeScript infers everything from the router.
Prisma: the ORM that changes everything
Multi-tenant auto-scoping
// ctx.orgDb automatically filters by organizationId
const clients = await ctx.orgDb.client.findMany();
// SQL: SELECT * FROM "Client" WHERE "organizationId" = 'org_xxx'Migrations
# Add a field
npx prisma migrate dev --name add-invoice-due-date
# Regenerate the typed client
npx prisma generateGranular RBAC pattern
// Permission-based access control
import { requirePermission } from "~/server/api/trpc";
export const invoiceRouter = createTRPCRouter({
// Only users with "facturation:create" can create one
create: requirePermission("facturation:create")
.input(createInvoiceSchema)
.mutation(async ({ ctx, input }) => {
// ...
}),
// Reading: "facturation:read" permission
getAll: requirePermission("facturation:read").query(async ({ ctx }) => {
// ...
}),
});Conclusion
This trio (Next.js 15 + tRPC + Prisma) gives you:
- Type safety from the database all the way to the React component
- Zero boilerplate: no duplicated types
- Great DX: autocomplete everywhere
- Performance: Server Components + optimized queries
Up next: implementing multi-tenant authentication with NextAuth v5.
Go further
Related articles
Multi-Tenant Isolation with Prisma and tRPC: What the Automatic Filter Covers, and What Doesn't
Isolate each organization's data in Next.js with Prisma $extends and tRPC: what the automatic filter covers, and what stays explicit.
ReadTesting a Multi-Tenant SaaS: 7 Essential Vitest Patterns
From tenant isolation to RBAC permission checks, the test patterns that make a B2B SaaS genuinely reliable. With Vitest, tRPC, and Prisma.
ReadCI/CD with GitHub Actions for a Next.js SaaS: One Workflow File, Explained
How a real Next.js SaaS pipeline works in GitHub Actions: affected-package Turbo filtering, cost-conscious PR-only triggers, and what gates every merge.
ReadReady to launch your SaaS?
HeartCo Starter includes everything you need: auth, payments, AI, mobile, audited security. Starting at $219.