REST vs tRPC vs GraphQL for Next.js: Choosing the Right API Layer
The API layer decision shapes your codebase for years. Each approach has real tradeoffs — here's how to evaluate them for a Next.js project honestly.
Nextcraft Engineering Team
The Context That Changes Everything
The right API approach depends heavily on your situation:
- Building a Next.js app that only your Next.js frontend consumes? Different answer than building an API that multiple clients (web, mobile, third parties) need.
- Team of 2? Different answer than team of 20.
- Internal tool? Different answer than public-facing product with partner integrations.
There's no universally correct answer. But there are patterns that consistently work for specific contexts.
Server Actions: The New Default for Internal APIs
Before evaluating REST vs tRPC vs GraphQL, consider whether you need an API layer at all.
For operations that only your Next.js application performs (form submissions, mutations, simple reads), Server Actions eliminate the need for an explicit API:
// lib/actions/projects.ts
'use server';
export async function createProject(name: string) {
const user = await requireAuth();
const project = await db.project.create({
data: { name, ownerId: user.id },
});
revalidatePath('/dashboard');
return project;
}
// Client Component — no API endpoint needed
'use client';
import { createProject } from '@/lib/actions/projects';
export function CreateProjectButton() {
return (
<button onClick={() => createProject('My New Project')}>
Create Project
</button>
);
}
Server Actions are:
- Type-safe by default (TypeScript across the boundary)
- Simpler than REST (no URL routing, no HTTP method decision)
- Integrated with Next.js caching and revalidation
- Not suitable for public APIs or mobile clients
Use Server Actions for: Form handling, mutations, internal data operations in a Next.js-only app.
tRPC: Type-Safe APIs Without the Schema Overhead
tRPC lets you call server-side functions from the client as if they were local functions, with full TypeScript inference — no code generation step.
// server/routers/project.ts
import { z } from 'zod';
import { router, protectedProcedure } from '../trpc';
export const projectRouter = router({
list: protectedProcedure.query(async ({ ctx }) => {
return ctx.db.project.findMany({
where: { ownerId: ctx.user.id },
});
}),
create: protectedProcedure
.input(z.object({ name: z.string().min(1) }))
.mutation(async ({ input, ctx }) => {
return ctx.db.project.create({
data: { name: input.name, ownerId: ctx.user.id },
});
}),
});
// Client Component — fully typed, autocompleted
import { trpc } from '@/lib/trpc';
export function ProjectList() {
const { data: projects } = trpc.project.list.useQuery();
const createProject = trpc.project.create.useMutation();
return (
<div>
{projects?.map(p => <div key={p.id}>{p.name}</div>)}
<button onClick={() => createProject.mutate({ name: 'New Project' })}>
Create
</button>
</div>
);
}
Strengths:
- Zero type duplication — one source of truth
- Excellent DX with autocomplete on procedure names and input shapes
- React Query integration built in (caching, loading states, invalidation)
- Perfect for full-stack TypeScript teams
Weaknesses:
- TypeScript-only — no good story for mobile clients or third-party integrations
- Bundle size overhead from tRPC client library
- Learning curve for teams unfamiliar with RPC patterns
Use tRPC when: You have a TypeScript Next.js frontend and no plans for external API consumers.
REST: The Interoperable Default
REST with Next.js Route Handlers is the right choice when:
- Other clients need your API (mobile apps, partner integrations)
- Your team knows REST deeply and the productivity tradeoffs favor familiarity
- You need fine-grained HTTP semantics (specific caching headers, content negotiation)
// app/api/projects/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
const user = await requireAuth(request);
const projects = await db.project.findMany({
where: { ownerId: user.id },
});
return NextResponse.json(projects, {
headers: { 'Cache-Control': 'private, max-age=60' },
});
}
export async function POST(request: NextRequest) {
const user = await requireAuth(request);
const body = await request.json();
const parsed = CreateProjectSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{ error: parsed.error.flatten() },
{ status: 422 }
);
}
const project = await db.project.create({
data: { ...parsed.data, ownerId: user.id },
});
return NextResponse.json(project, { status: 201 });
}
For REST APIs in TypeScript, pair with zod-openapi or similar to generate OpenAPI specs and client SDKs — otherwise you're manually maintaining the contract that tRPC gives you for free.
GraphQL: When Flexibility Justifies the Cost
GraphQL makes sense when:
- You have many different clients with different data requirements (mobile app needs minimal data, web app needs full relations, partner integrations need custom shapes)
- You have many interconnected entity types with complex relationships
- Your team is already productive in GraphQL
For most Next.js SaaS applications: GraphQL is overengineering. The setup cost (schema definition, resolver layer, code generation, client configuration) is substantial. The benefits (client-defined queries, type-safe generated clients) are real but available through other means (tRPC, well-designed REST) at lower cost.
Use GraphQL when: You have multiple diverse clients, complex graph-like data relationships, and a team experienced with GraphQL patterns.
The Decision Tree
Do you have mobile clients or external API consumers?
├── Yes → REST (or GraphQL if data is highly graph-like)
└── No (Next.js only)
├── TypeScript team, medium complexity → tRPC
├── Simple CRUD, team prefers simplicity → Server Actions
└── Need HTTP semantics / external tooling → REST
Most new Next.js SaaS projects in 2026: start with Server Actions for mutations, tRPC for complex queries if you need a separate client layer, and REST only when you add non-Next.js clients.
Don't build a GraphQL API because a blog post said so. Build it because your specific data requirements and client diversity justify the cost.
Continue reading
Related articles
Why Next.js App Router Is Better for SEO Than Pages Router
The App Router isn't just a new file-system convention — it fundamentally changes how search engines crawl and index your Next.js application.
EngineeringServer Components vs Client Components: Making the Right Call
The boundary between Server and Client Components is the most consequential architectural decision you make in a Next.js application. Here's how to draw it correctly.
EngineeringBuilding High-Performance Next.js Applications for Scale
A deep dive into how we utilize App Router and React Server Components to scale our client builds effectively.
Stay Informed.
Join 1,200+ founders and engineers receiving our monthly deep dives on product engineering, design, and growth.