EmpireUI
Get Pro
← Blog9 min read#trpc#rest#graphql

tRPC vs REST vs GraphQL in 2026: Which API Layer Should You Use?

tRPC, REST, and GraphQL each make real trade-offs. Here's an honest breakdown of which API layer actually fits your stack in 2026 — no hype, just facts.

Developer writing API code on a laptop screen at night

The API Layer Debate Is Still Not Settled

Every year someone declares one of these three dead. REST is "too verbose", GraphQL is "over-engineered", tRPC is "only for TypeScript purists". None of that is true, and none of it is the full picture. The real question isn't which one wins — it's which one fits *your* project, *your* team, and the constraints you're actually working under right now in 2026.

That said, the ecosystem has shifted meaningfully since 2023. tRPC v11 landed with first-class Next.js App Router support. GraphQL has matured into something genuinely pleasant with tools like Pothos and Houdini. REST hasn't changed — which is either a feature or a bug depending on your perspective. All three are production-ready. All three have real trade-offs.

This isn't a horse-race article where one option "wins". You'll walk away knowing exactly when to reach for each one. Let's be precise about that.

REST: The Baseline You Already Know

REST is the default. It's what your brain reaches for when you spin up an Express server or write a Next.js route handler. Every HTTP client speaks it, every language has tooling for it, every proxy and CDN understands it. That ubiquity isn't boring — it's genuinely valuable when you're building a public API or integrating with third-party consumers you don't control.

The rough part is the lack of a contract. You define a GET /users/:id endpoint and you return whatever JSON you feel like returning that day. The consumer has to trust your docs — if you even wrote them. Add a field, rename a field, change the shape of a nested object? Good luck knowing who breaks. In a small team with good discipline it's fine. In a team of 15 with four frontend devs pulling data from the same service, it gets painful fast.

Worth noting: OpenAPI / Swagger has tried to solve the contract problem, and it does — to a point. Generating types from an OpenAPI spec with openapi-typescript gives you something close to what tRPC gives you natively. It's about 200 extra lines of config and a generation step, but it works. If you're already invested in OpenAPI, that investment is real and shouldn't be thrown out lightly.

// A plain Next.js REST route — nothing fancy, but everyone can read it
export async function GET(req: Request) {
  const { searchParams } = new URL(req.url);
  const id = searchParams.get('id');
  const user = await db.user.findUnique({ where: { id: Number(id) } });
  return Response.json(user);
}

Honestly, REST is the right call when you have external consumers — mobile apps from another team, third-party integrations, public developer APIs. The moment your API surface is "public" in any sense, REST's predictability and tooling ecosystem become its biggest advantages.

GraphQL: Powerful, But Bring a Spare Weekend

GraphQL was supposed to solve the over-fetching and under-fetching problems that REST creates. And it does — when it's set up well. You get a typed schema, introspection, a single endpoint, and clients that can ask for exactly the fields they need. Facebook built it for good reason. The problem isn't the spec. The problem is the surface area you take on when you adopt it.

Setting up a production GraphQL server means picking a schema-definition approach (SDL-first vs code-first), a server library (Apollo, Yoga, Pothos), a caching strategy that doesn't blow up on N+1 queries (DataLoader is mandatory, not optional), and a client-side story (URQL, Apollo Client, or raw fetch). Each of those choices has real opinions attached. You can spend three days doing nothing but plumbing before you write a single resolver.

In practice, GraphQL pays off at scale. If you have a large, heterogeneous data graph — multiple microservices stitched together, many different frontend clients with different data needs — the federation story is genuinely excellent. Apollo Federation v2 and GraphQL Yoga's subgraph support have made multi-service GraphQL much less painful than it was in 2020. But you need the scale to justify the overhead.

# Schema definition — readable, self-documenting
type User {
  id: ID!
  name: String!
  email: String!
  posts: [Post!]!
}

type Query {
  user(id: ID!): User
  users(limit: Int = 20): [User!]!
}
// Resolver with DataLoader to avoid N+1
const resolvers = {
  Query: {
    user: (_, { id }, { db }) => db.user.findUnique({ where: { id } }),
  },
  User: {
    posts: ({ id }, _, { loaders }) => loaders.postsByUserId.load(id),
  },
};

Quick aside: if you're building something design-intensive and you want to see how API shape affects UI flexibility, look at how Empire UI's glassmorphism components handle composable props. The principle is the same — granular control over what you request is valuable, but only when the thing requesting has real variance in what it needs.

tRPC: The TypeScript Stack's Best Trick

tRPC is not a transport protocol. It's not replacing HTTP. It's a thin layer that gives you end-to-end type safety from your server procedures to your client calls — no code generation, no schema files, just TypeScript inference doing what TypeScript inference does. If you rename a procedure or change a return type, your editor immediately screams at every call site. That feedback loop is genuinely addictive once you've felt it.

The constraint is obvious: both your client and your server have to be TypeScript. Specifically, they have to share the same router type — which means they need to live in the same monorepo or at least have access to the same type package. If you have a Go backend or a public mobile SDK consuming your API, tRPC is a non-starter for that surface. Full stop.

That said, the Next.js + tRPC combo in 2026 is probably the fastest way to build a full-stack TypeScript app with zero API contract drift. With tRPC v11 and React Query integration, your client hooks are generated from your router definition at zero runtime cost. You write a getUserById procedure, and you call trpc.getUserById.useQuery({ id }) on the client. The types flow through. It's not magic — it's just inference, but it removes an entire category of bugs.

// server/routers/user.ts
import { z } from 'zod';
import { router, publicProcedure } from '../trpc';

export const userRouter = router({
  getById: publicProcedure
    .input(z.object({ id: z.number() }))
    .query(async ({ input, ctx }) => {
      return ctx.db.user.findUnique({ where: { id: input.id } });
    }),

  create: publicProcedure
    .input(z.object({ name: z.string().min(1), email: z.string().email() }))
    .mutation(async ({ input, ctx }) => {
      return ctx.db.user.create({ data: input });
    }),
});
// components/UserProfile.tsx — fully typed, no codegen
import { trpc } from '~/utils/trpc';

export function UserProfile({ id }: { id: number }) {
  const { data, isLoading } = trpc.user.getById.useQuery({ id });

  if (isLoading) return <Skeleton />;
  // data.name is typed — no guessing, no casting
  return <h2>{data?.name}</h2>;
}

One more thing — tRPC's input validation via Zod means you get runtime validation *and* TypeScript types from the same definition. You write the schema once and it protects both ends. That single fact eliminates a huge class of "the frontend sent bad data" bugs.

The Decision Matrix: Which One When

Here's the honest breakdown. You're building a full-stack TypeScript monorepo — Next.js, Remix, or a Vite SPA with a Node backend — and you have no external consumers? Pick tRPC. The DX advantage is substantial, the overhead is minimal, and you'll ship faster without the discipline tax REST requires or the plumbing tax GraphQL demands.

You're building a public API — a developer platform, a SaaS product with third-party integrations, a service consumed by mobile apps you don't control? Use REST with OpenAPI. Generate types with openapi-typescript. Document it. The interoperability is worth the boilerplate.

You have a complex data graph with multiple services, multiple frontends with genuinely different data needs, and a team large enough to own the infrastructure? GraphQL federation is the right answer. The investment pays off at that scale. Below that scale — say, fewer than five services and two frontend apps — you're probably paying the tax without getting the dividend.

| Scenario | Pick | |---|---| | Full-stack TS monorepo, internal only | tRPC | | Public API, external consumers | REST + OpenAPI | | Large data graph, multiple services | GraphQL | | Mixed TS/non-TS backends | REST | | Rapid prototype, single team | tRPC |

Look, the worst outcome isn't picking the "wrong" one — it's agonizing over the choice for two weeks while you could be building. All three are solid. Pick one that matches your actual constraints, not your aspirational architecture.

Performance: What the Benchmarks Actually Tell You

Raw throughput benchmarks — requests per second on a Node server — show REST and tRPC performing nearly identically. tRPC is REST under the hood; it serializes to JSON and sends it over HTTP. The overhead is TypeScript inference at compile time, not runtime weight. You're not paying a performance penalty for the type safety.

GraphQL has a real performance surface area to manage: query parsing, validation, and the N+1 problem. A naive GraphQL implementation hitting a Postgres database can be catastrophically slow — 50ms queries turning into 5000ms because every nested resolver makes a separate DB call. DataLoader solves it, but you have to know to use it and know to use it correctly. With REST or tRPC, the query shaping is explicit in your code; you're unlikely to write an N+1 by accident.

Worth noting: for bandwidth-sensitive clients — older mobile devices, markets with slow 4G — GraphQL's ability to fetch exactly the fields you need can make a real difference in payload size. A REST endpoint returning 40 fields when you need 6 wastes 80px of margin in your mobile layout and costs someone bandwidth money. It's the original use case, and it still applies in the right context.

The gradient generator tool on Empire UI fetches live preview data through a simple REST endpoint for exactly this reason — external embeds need to call it without any TypeScript assumptions. If that data layer were tRPC-only, we'd have a problem the moment a third-party tried to integrate.

Mixing Them: Yes, You Can Do That

Nothing forces you to pick one and live with it for your entire app. The most pragmatic architecture in 2026 is often: tRPC for internal full-stack product features, REST for your public-facing API surface, and maybe a thin GraphQL gateway if you're stitching together microservices that different teams own. These aren't mutually exclusive religions.

A Next.js app can run tRPC route handlers at /api/trpc/* and standard REST handlers at /api/v1/* in the same codebase, no conflict. Your internal dashboard uses tRPC. Your webhooks receiver and public API use REST. The decision is per-surface, not per-project.

The Empire UI component library itself follows this pattern — internal tooling talks to typed procedures, but the CDN endpoints and public widget scripts use plain REST because the consumers (marketing sites, embeds, third-party users) have no TypeScript context at all. Real-world constraints drive real-world architecture.

One more thing — if you do mix, document the boundary clearly. Future-you, or your next hire, needs to know *why* /api/trpc exists alongside /api/v1. A 10-line comment in your README explaining the split saves hours of confusion. That's not bureaucracy; that's kindness.

FAQ

Can tRPC work with a non-TypeScript frontend like plain React or Vue without TypeScript?

Technically yes — you can call tRPC's underlying HTTP endpoints like any REST API. But you lose all the type inference that makes tRPC worth using. At that point you're just adding complexity for no gain. Use REST instead.

Is GraphQL overkill for a small SaaS app?

Almost certainly. For a small product with one or two frontend clients and a handful of services, the schema management, resolver setup, and N+1 prevention overhead outweigh the benefits. Start with tRPC or REST, and migrate if your data graph genuinely demands it.

Does tRPC v11 work with the Next.js App Router without a separate Express server?

Yes. tRPC v11 ships official Next.js App Router adapters. You define your router and mount it as a Route Handler — no separate server process needed. The server component and client component integration works cleanly with React Query v5.

What's the best option if my team includes both backend devs who don't write TypeScript and frontend devs who do?

REST with OpenAPI. Generate TypeScript types for the frontend from the spec using openapi-typescript. Your backend team works in whatever language they prefer, your frontend team gets typed client code, and the OpenAPI spec is the shared contract between them.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

Hono vs Express in 2026: Edge-Ready vs Battle-TestedNext.js vs Remix in 2026: Which One Should You Use?tRPC + Next.js Guide: End-to-End Type Safety Without GraphQLTailwind vs CSS Modules in 2026: Which One Should You Actually Use?