GraphQL vs REST vs tRPC 2026: Which API Should You Build?
GraphQL vs REST vs tRPC 2026 comparison: when to use each, performance, type safety, caching, tooling, and decision guide for your next API.
Choosing the wrong API architecture is expensive. Migrating from REST to GraphQL mid-project β or the reverse β costs weeks. This guide cuts through the noise: hereβs when REST, GraphQL, and tRPC each win, with side-by-side code comparisons for the same data fetch.
The Contenders
REST (Representational State Transfer) β the default choice since the early 2000s, resource-based, HTTP-native.
GraphQL β a query language for APIs developed by Meta, single endpoint, client-driven data fetching.
tRPC β TypeScript Remote Procedure Call, zero-schema end-to-end type safety for TypeScript-first full-stack apps.
REST: The Universal Standard
Pros
- Simple mental model β everyone understands HTTP verbs and status codes
- Native caching β GET requests cache at browser, CDN, and proxy layers without configuration
- Universal client support β curl, Postman, any HTTP library in any language
- Stateless β each request is self-contained, scales horizontally with ease
- Great for public APIs β no SDK required, any developer can consume it
Cons
- Over-fetching β endpoints return fixed shapes; you often get more data than needed
- Under-fetching β complex UIs require multiple round trips to assemble data
- No built-in type safety β contract drift between server and client is easy to introduce
- Schema evolution is manual β adding fields is safe, removing requires versioning discipline
Example: Fetch a user and their latest 3 orders
// REST: requires 2 round trips
const user = await fetch('/api/users/123').then(r => r.json());
const orders = await fetch('/api/users/123/orders?limit=3').then(r => r.json());
// Assemble on client
const profile = { ...user, recentOrders: orders.data };
GraphQL: Flexible Data Fetching
Pros
- Precise data fetching β clients request exactly the fields they need, nothing more
- Single endpoint β one URL handles all queries and mutations
- Self-documenting β introspection provides a live schema explorer
- Excellent for complex data graphs β deeply nested relational data in one request
- Strong ecosystem β Apollo, Relay, GraphQL Yoga, Pothos, code generation
Cons
- Caching complexity β POST-based queries donβt cache automatically; requires Apollo Client or persisted queries
- N+1 problem β naive resolvers fire one DB query per list item (solved with DataLoader, but requires setup)
- Overly flexible β clients can craft expensive queries; depth limiting and cost analysis needed
- Overkill for simple APIs β heavy setup cost for CRUD-heavy services
- TypeScript-only safety β requires codegen to maintain type sync
Example: Same fetch in GraphQL
// GraphQL: one round trip, exact fields
const { data } = await client.query({
query: gql`
query GetUserProfile($userId: ID!) {
user(id: $userId) {
id
name
email
avatar
orders(first: 3, orderBy: CREATED_AT_DESC) {
edges {
node {
id
total
status
createdAt
}
}
}
}
}
`,
variables: { userId: '123' }
});
tRPC: End-to-End Type Safety Without a Schema
Pros
- Zero-schema type safety β TypeScript types flow from server to client automatically via inference
- No code generation step β types are live, always in sync
- Excellent DX β autocomplete on API calls in your editor
- Lightweight β no runtime schema parsing overhead
- First-class React Query integration β built-in caching, loading states, optimistic updates
Cons
- TypeScript-only β not usable from non-TypeScript clients (no mobile apps, no third-party consumers)
- Monorepo-friendly, not API-platform-friendly β designed for apps that own both client and server
- No public API use case β no documentation format, no SDK generation
- Tighter coupling β server and client must share code or be in the same repo
Example: Same fetch in tRPC
// Server: define procedure
export const appRouter = router({
userProfile: publicProcedure
.input(z.object({ userId: z.string() }))
.query(async ({ input }) => {
const user = await db.user.findUnique({ where: { id: input.userId } });
const orders = await db.order.findMany({
where: { userId: input.userId },
take: 3,
orderBy: { createdAt: 'desc' }
});
return { ...user, recentOrders: orders };
}),
});
// Client: fully typed, autocomplete works
const { data } = trpc.userProfile.useQuery({ userId: '123' });
// data.recentOrders[0].status β TypeScript knows the shape!
Side-by-Side Comparison
| Dimension | REST | GraphQL | tRPC |
|---|---|---|---|
| Type safety | Manual / OpenAPI | Codegen required | Automatic inference |
| Caching | Native HTTP | Manual setup | Via React Query |
| Public API | Excellent | Good | Not suitable |
| Learning curve | Low | Medium | Low (if you know TypeScript) |
| Over/under-fetching | Common issue | Solved | Non-issue (server controls shape) |
| Language support | Any | Any | TypeScript only |
| Schema | OpenAPI | SDL | None needed |
| Best for | Public APIs, microservices | Complex graphs, mobile BFF | Full-stack TS monorepos |
Decision Guide: Which Should You Build?
Choose REST when:
- Your API will be consumed by third parties or mobile apps you donβt control
- You need simple, well-understood HTTP semantics
- Caching at the CDN layer matters for performance
- Your team spans multiple languages or backgrounds
- Youβre building a public developer platform (like Stripe, GitHub, Twilio)
Choose GraphQL when:
- Your data is a complex graph with many relationships (social networks, e-commerce, CMS)
- You serve multiple clients (web, iOS, Android) with different data needs
- You want to consolidate multiple REST services behind a single gateway
- You have a dedicated frontend team that needs data fetching flexibility
- Real-time subscriptions are a core feature
Choose tRPC when:
- Your entire stack is TypeScript (Next.js, Node, or similar)
- You control both the client and server
- You want maximum developer experience with zero schema boilerplate
- Youβre building an internal tool, SaaS dashboard, or startup MVP
- You already use React Query and want tighter integration
Migration Considerations
REST β GraphQL
Introduce a GraphQL layer on top of existing REST services. Use schema stitching or federation to aggregate gradually. Donβt attempt a big-bang rewrite.
REST β tRPC
Works best in greenfield projects or when migrating a Next.js app. Extract existing REST handlers into tRPC procedures iteratively.
GraphQL β tRPC
Unusual, but possible if youβve outgrown the complexity. Replace queries/mutations one-by-one with tRPC procedures in a TypeScript monorepo.
The Honest Take for 2026
REST isnβt dying. GraphQL isnβt winning. tRPC isnβt for everyone.
The best choice depends on who consumes your API and whether you control both ends. Public API with external consumers? REST with a solid OpenAPI spec. Internal full-stack TypeScript app? tRPC eliminates entire categories of bugs. Multi-client product with a complex data model? GraphQL pays off at scale.
Most production systems use a combination: REST for public-facing endpoints, tRPC for internal BFF layers, and sometimes GraphQL as an aggregation gateway over legacy services.
Pick the right tool, not the trendy one.
50 CLI Commands That Save Hours Every Week
The exact Git shortcuts, file search tricks, Docker commands, and productivity hacks that senior developers use daily. Compiled from 5+ years of shipping production code.
- Git mastery (10 cmds)
- File & search wizardry
- Docker & DevOps tricks
- Process & network tools
- Productivity boosters
- macOS & Linux install guide
No spam. 1 email/week max. Unsubscribe anytime.
Found this guide useful? Check out our free developer tools.
Affiliate disclosure: Some links below are affiliate links β we may earn a small commission at no extra cost to you. Learn more.
Recommended Tools & Resources
DigitalOcean
$200 credit for new users. Simple, affordable cloud hosting for developers.
GitHub Student Pack
Free access to 100+ developer tools. Perfect for students and new devs.
Vercel
Deploy frontend apps instantly. Free tier is generous for side projects.
DevPlaybook Products
Boilerplates, scripts & AI toolkits to 10x your dev workflow.