tRPC vs REST vs GraphQL: API Layer in SaaS Boilerplates in 2026
TL;DR
tRPC for internal TypeScript full-stack apps — end-to-end type safety with minimal boilerplate. REST for public APIs, multiple client languages, or when you need cache-ability. GraphQL for complex data fetching requirements, multiple clients with different data needs, or large teams. Most indie SaaS boilerplates use tRPC or Server Actions in 2026.
The Shift in 2026
The API landscape in SaaS boilerplates has shifted dramatically:
2020: REST dominates
2022: GraphQL grows (Apollo)
2023: tRPC explodes (T3 Stack popularization)
2024: Server Actions enter (Next.js App Router)
2026: tRPC + Server Actions share the space; REST for public APIs
tRPC: Type-Safe RPCs Without Code Generation
tRPC gives you a type-safe API layer between your Next.js frontend and backend — no schema files, no code generation, no manual type maintenance.
// server/api/routers/post.ts — define procedures
export const postRouter = createTRPCRouter({
create: protectedProcedure
.input(z.object({
title: z.string().min(1).max(100),
content: z.string().min(1),
}))
.mutation(async ({ ctx, input }) => {
return ctx.db.post.create({
data: {
title: input.title,
content: input.content,
authorId: ctx.session.userId,
},
});
}),
getAll: protectedProcedure.query(async ({ ctx }) => {
return ctx.db.post.findMany({
where: { authorId: ctx.session.userId },
orderBy: { createdAt: 'desc' },
});
}),
});
// Client — full TypeScript inference, no manual type imports
const utils = api.useUtils();
const { data: posts } = api.post.getAll.useQuery();
// posts: Post[] | undefined — TypeScript knows the exact return type
const createPost = api.post.create.useMutation({
onSuccess: () => utils.post.getAll.invalidate(),
});
createPost.mutate({ title: 'Hello', content: 'World' });
// TypeScript validates input shape at compile time
tRPC Advantages
- Zero API maintenance: Change the server return type → TypeScript errors on client immediately
- No fetch boilerplate: No
fetch('/api/posts').then(r => r.json())— just call the procedure - React Query integration: Built-in caching, optimistic updates, background refetch
- Input validation: Zod schemas serve as both TypeScript types AND runtime validation
tRPC Limitations
- TypeScript only (no cross-language clients)
- HTTP/1 limitations (no true HTTP caching, all POSTs)
- No public API: tRPC isn't appropriate for customer-facing APIs
- Context coupling: tightly coupled to your framework/runtime
Server Actions: The App Router Alternative
Next.js Server Actions (stable in Next.js 14+) provide a way to call server functions from Client Components without a separate API layer:
// app/actions/post.ts — Server Actions
'use server';
import { auth } from '@clerk/nextjs/server';
export async function createPost(formData: FormData) {
const { userId } = await auth();
if (!userId) throw new Error('Unauthorized');
const title = formData.get('title') as string;
const content = formData.get('content') as string;
return prisma.post.create({
data: { title, content, authorId: userId }
});
}
// Client Component — calls server function directly
'use client';
import { createPost } from '@/app/actions/post';
export function CreatePostForm() {
return (
<form action={createPost}>
<input name="title" />
<textarea name="content" />
<button type="submit">Create</button>
</form>
);
}
Server Actions are compelling for form-based interactions but lack the caching/invalidation ergonomics of tRPC + React Query.
tRPC vs Server Actions:
- tRPC: better for complex data fetching, React Query caching, optimistic updates
- Server Actions: simpler for form submissions, mutations without optimistic UI
REST: The Reliable Standard
REST remains the right choice for:
- Public APIs that external developers consume
- Mobile clients (React Native, Flutter) where tRPC's coupling is undesirable
- Multiple client languages (a Python data pipeline calling your API)
- Webhooks you receive (Stripe, GitHub — always REST)
// app/api/v1/posts/route.ts — Standard REST endpoint
import { NextRequest } from 'next/server';
import { z } from 'zod';
const createPostSchema = z.object({
title: z.string().min(1).max(100),
content: z.string().min(1),
});
export async function POST(req: NextRequest) {
const body = await req.json();
const result = createPostSchema.safeParse(body);
if (!result.success) {
return Response.json({ error: result.error.flatten() }, { status: 400 });
}
const post = await prisma.post.create({ data: result.data });
return Response.json(post, { status: 201 });
}
export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url);
const page = parseInt(searchParams.get('page') ?? '1');
const limit = Math.min(parseInt(searchParams.get('limit') ?? '20'), 100);
const posts = await prisma.post.findMany({
skip: (page - 1) * limit,
take: limit,
orderBy: { createdAt: 'desc' },
});
return Response.json({ posts, page, limit });
}
The downside: manual type maintenance. Change the response shape on the server → TypeScript doesn't tell the client until a test fails or a user reports a bug.
GraphQL: Powerful but Heavy
GraphQL excels when multiple clients with different data needs query the same API:
# Client 1 (dashboard) — needs minimal user data
query {
user(id: "123") {
id
name
plan
}
}
# Client 2 (admin) — needs full user data
query {
user(id: "123") {
id
name
email
plan
createdAt
subscription {
status
currentPeriodEnd
}
invoices(limit: 10) {
amount
paidAt
}
}
}
Both queries are served by the same resolver — clients fetch exactly what they need.
GraphQL in boilerplates: Rare. Apollo, Pothos, and similar are used in some enterprise boilerplates. For indie SaaS, the overhead (resolvers, schema, tooling) rarely pays off vs tRPC.
Choose GraphQL when: Multiple first-party clients with different data needs, external developer API with complex querying requirements, or team has existing GraphQL expertise.
Boilerplate API Choices
| Boilerplate | Primary API Layer |
|---|---|
| T3 Stack | tRPC |
| T3 Turbo | tRPC (shared across apps) |
| ShipFast | tRPC + REST (for webhooks) |
| Supastarter | tRPC + Supabase client |
| Epic Stack | Server Actions (Remix form approach) |
| Open SaaS | tRPC |
Decision Guide
Internal TypeScript full-stack app with one client?
→ tRPC (best DX, type safety, React Query)
Need a public API for external developers?
→ REST (OpenAPI spec)
Multiple first-party clients with complex data needs?
→ GraphQL or tRPC with separate client packages
Form-heavy Next.js App Router app?
→ Server Actions (simpler for mutations, pairs with tRPC for queries)
Mobile + web + backend as separate services?
→ REST or GraphQL
Find boilerplates by API architecture on StarterPick.
Check out this boilerplate
View T3 Stack on StarterPick →