Every new SaaS faces the same fork: pick an API paradigm, build on it for years. I've shipped production apps with all three in the last 18 months. Here's what actually matters when you're building alone or with a small team.
The Quick Answer
- tRPC: Best for TypeScript monorepos, solo founders, internal APIs
- REST: Best for public APIs, third-party integrations, multi-client
- GraphQL: Best for complex data graphs, mobile apps, large teams with dedicated API layer
If you're a solo founder building a Next.js SaaS in 2026: tRPC. The DX advantage is too large to ignore.
tRPC: End-to-End Types Without the Schema
// server/router.ts
import { router, publicProcedure, protectedProcedure } from './trpc';
import { z } from 'zod/v4';
export const appRouter = router({
user: router({
getById: protectedProcedure
.input(z.object({ id: z.string().uuid() }))
.query(async ({ input, ctx }) => {
return ctx.db.user.findUniqueOrThrow({ where: { id: input.id } });
}),
update: protectedProcedure
.input(z.object({
id: z.string().uuid(),
name: z.string().min(1).max(100),
email: z.email(),
}))
.mutation(async ({ input, ctx }) => {
return ctx.db.user.update({
where: { id: input.id },
data: { name: input.name, email: input.email },
});
}),
}),
});
export type AppRouter = typeof appRouter;
// client — zero code generation, full type safety
import { trpc } from '../utils/trpc';
function UserProfile({ userId }: { userId: string }) {
// Return type inferred automatically from the router definition
const { data: user } = trpc.user.getById.useQuery({ id: userId });
const updateUser = trpc.user.update.useMutation();
// user.name, user.email — fully typed, no codegen step
}
What you don't have to do:
- Write an OpenAPI spec
- Run a codegen step
- Maintain a separate client SDK
- Write fetch wrappers
Refactor the server: TypeScript errors appear on the client immediately. This is the tRPC advantage in one sentence.
The ceiling: tRPC only works when client and server share a TypeScript codebase. The moment you need a mobile app, a third-party integration, or a Python service calling your API — you need REST or a separate REST layer.
REST: Still the Right Answer for Public APIs
// Next.js App Router API route
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod/v4';
const CreateUserSchema = z.object({
name: z.string().min(1),
email: z.email(),
plan: z.enum(['free', 'pro', 'enterprise']),
});
export async function POST(req: NextRequest) {
const body = await req.json();
const parsed = CreateUserSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{ error: 'Validation failed', details: parsed.error.issues },
{ status: 400 }
);
}
const user = await db.user.create({ data: parsed.data });
return NextResponse.json(user, { status: 201 });
}
REST's strengths in 2026:
- Works with any client in any language
- HTTP caching (CDN, browser) works naturally
- OpenAPI + codegen gives typed clients for other teams
- Every debugging tool understands HTTP
REST's weakness for solo SaaS: you're writing a lot of boilerplate that tRPC would eliminate, and TypeScript errors don't propagate client-server automatically.
GraphQL: When the Data Graph Justifies the Complexity
// schema.graphql
type Query {
user(id: ID!): User
users(filter: UserFilter): [User!]!
}
type User {
id: ID!
name: String!
email: String!
orders(limit: Int): [Order!]!
subscription: Subscription
}
type Order {
id: ID!
total: Int!
items: [OrderItem!]!
}
Clients fetch exactly what they need:
# Mobile app — minimal fields
query GetUserCard {
user(id: "usr_123") {
name
subscription { plan }
}
}
# Dashboard — full data
query GetUserDetail {
user(id: "usr_123") {
name
email
orders(limit: 10) {
id
total
items { name quantity }
}
}
}
Same endpoint, different responses. No over-fetching.
GraphQL is the right choice when:
- You have 3+ client types (web, iOS, Android) with different data needs
- Your data has deep relationships (social graphs, content DAGs)
- You have a dedicated backend team that can own the schema
- You're building a developer-facing platform where query flexibility matters
GraphQL is overkill when:
- It's a solo project with one web client
- Your data is mostly flat CRUD
- You don't have time to configure a resolver layer, caching strategy, and N+1 query prevention
Real-World Performance: What Matters at SaaS Scale
Request overhead (rough benchmarks, similar query complexity):
tRPC: ~0.1ms (function call, same process in monorepo)
REST: ~1-3ms (HTTP parsing overhead, simpler serialization)
GraphQL: ~3-10ms (resolver tree, field resolution, N+1 if unguarded)
At SaaS scale, these differences are irrelevant compared to database query time. Don't optimize API protocol — optimize queries.
The Pattern I Use in 2026
Internal API (Next.js ↔ own frontend): tRPC
Public API (third-party devs): REST + OpenAPI
Webhooks (Stripe, GitHub, etc.): REST endpoints
LLM tool calls: REST (Claude/OpenAI expect JSON schemas)
tRPC handles 90% of the surface area with 10% of the code. REST handles the edges where tRPC doesn't reach.
Migration Risk
One thing nobody talks about: switching API paradigms is expensive. Choose with a 2-year horizon.
tRPC → REST: doable (it's just HTTP underneath), but you lose type propagation
REST → GraphQL: significant — resolver layer, schema design, client query refactoring
GraphQL → REST: usually a sign something went wrong in selection
Ship Your API Faster
The AI SaaS Starter Kit ($99) ships with tRPC pre-configured alongside Next.js App Router — auth middleware, Zod validation, and Drizzle ORM wired up end-to-end. Skip the 2-day setup.
Building automations that need to call your API? The Workflow Automator MCP ($15/mo) integrates with REST and tRPC backends so your AI workflows are as type-safe as your app.
If I were starting a SaaS today with one developer: tRPC, Next.js App Router, Drizzle, Supabase. That stack gets you to $10k MRR without touching API design again.
What stack are you shipping on in 2026?
Top comments (0)