Skip to main content

tRPC vs REST vs GraphQL: API Layer in SaaS Boilerplates in 2026

·StarterPick Team
trpcrestgraphqlapi2026

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

BoilerplatePrimary API Layer
T3 StacktRPC
T3 TurbotRPC (shared across apps)
ShipFasttRPC + REST (for webhooks)
SupastartertRPC + Supabase client
Epic StackServer Actions (Remix form approach)
Open SaaStRPC

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 →

Comments