← Back to blog
··15 min read

REST vs GraphQL vs tRPC in 2026: Which API Layer Should You Build On

RESTGraphQLtRPCAPITypeScriptNext.jsFull StackBackend
REST vs GraphQL vs tRPC in 2026: Which API Layer Should You Build On

Three Ways to Draw the Line Between Client and Server

Every full-stack app has a seam: the point where the frontend stops and the backend begins. How you design that seam — the shape of the requests, who is allowed to call it, how much the two sides know about each other's types — is one of the highest-leverage architecture decisions you'll make. It's hard to change later, and it colours everything from developer speed to how buyers judge your code.

In 2026, three approaches dominate that seam in the TypeScript and Next.js world, and they make fundamentally different bets:

  • REST — the universal standard. Resources at URLs, plain HTTP methods, works with every language and every cache on earth. Simple, boring in the best way, and consumable by anyone.
  • GraphQL — the flexible query layer. One endpoint, a schema, and clients that ask for exactly the fields they want in a single request. Built for large graphs and many different clients.
  • tRPC — the TypeScript shortcut. Call your backend procedures like typed functions, share the types through a single import, and get end-to-end type safety with no schema and no code generation.
  • Pick the wrong one and the symptoms are predictable: you expose a tRPC "API" that a mobile team can't consume, you stand up a full GraphQL server for an app with one client that only ever needs fixed data, or you hand-roll dozens of REST endpoints and spend your life keeping frontend types in sync with backend responses by hand.

    This guide compares all three through two lenses: which is best to build on, and which produces a codebase that's clean to hand off or sell.

    Network cables and server infrastructure representing API connections

    Network cables and server infrastructure representing API connections

    First, The Distinction That Makes This Whole Category Make Sense

    Before comparing features, the single most important point: tRPC is not the same kind of thing as REST and GraphQL.

  • REST and GraphQL are protocols. They define a wire format — HTTP verbs and JSON for REST, a query language and schema for GraphQL — that *any* client in *any* language can speak. A Swift app, a Python service, a partner's integration, a cURL command: all can consume them without knowing anything about your backend's implementation.
  • tRPC is a TypeScript-to-TypeScript shortcut. It erases the client/server boundary *inside one codebase* by having the client import the server's types directly. There's no schema to publish and no separate contract — the types *are* the contract. That's its superpower and its hard limit: it only works when the same team owns both ends and both are TypeScript.
  • Get this right and the whole decision clarifies. The question isn't "which is fastest" or "which is trendiest" — it's who consumes this API? If the answer is "my own TypeScript frontend, and only that," tRPC is built for you. If the answer includes "other languages, mobile apps, third parties, or the public," you need a real protocol: REST or GraphQL. This is the same coherence-over-hype thinking behind picking a sensible tech stack for web apps in 2026.

    At a Glance

    RESTGraphQLtRPC
    First popularizedEarly 2000s2015 (Facebook)2020
    What it isProtocol (resources + HTTP)Protocol (query language)TypeScript RPC layer
    ConsumersAny language, anyoneAny language, anyoneTypeScript clients you own
    Type safetyManual (or via OpenAPI)Via schema + codegenEnd-to-end, no codegen
    Over-fetchingCommonSolved (pick fields)Per-procedure shape
    HTTP cachingNative (GET URLs)Harder (single POST)Via batching, not CDN
    Server complexityLowHigh (schema, resolvers)Low
    Public APIExcellentExcellentNo
    Best forPublic / polyglot APIsLarge multi-client graphsTypeScript full-stack you own

    Note: all three evolve, and each has a large ecosystem of frameworks and conventions. Treat this table as a map, not a spec sheet — verify current behaviour against the official docs before you commit.

    The Design Decision That Explains Each One

    Almost every practical difference follows from one bet each approach made.

    REST: model everything as resources anyone can address

    REST's bet is that a uniform, resource-oriented interface over plain HTTP is the most durable, universal way to expose data. You have nouns (resources) at URLs, and you act on them with standard verbs.

    ts
    // REST — resources and verbs, consumable by anything
    GET    /api/users          // list users
    GET    /api/users/1        // one user
    POST   /api/users          // create
    PATCH  /api/users/1        // update
    DELETE /api/users/1        // delete
    GET    /api/users/1/orders // that user's orders

    What you get is universality and simplicity: every language, framework, and tool speaks HTTP, cacheable GET URLs get free CDN and browser caching, and there's almost nothing conceptual to learn. What you pay is over-fetching and under-fetching — an endpoint returns a fixed shape (often more than a screen needs), and assembling one view from several resources means several round trips or a bespoke endpoint. Type safety is also on you: unless you layer OpenAPI and code generation on top, nothing stops the frontend and backend types from drifting apart.

    REST is the right answer when your API is public, polyglot, or cacheable-by-nature — the safe, universal default for anything the outside world consumes.

    GraphQL: let the client ask for exactly what it needs

    GraphQL's bet is that with many clients pulling different slices of a large, interconnected data graph, the server shouldn't dictate fixed response shapes. Instead, one endpoint exposes a typed schema, and each client sends a query for precisely the fields it wants.

    graphql
    # GraphQL — one request, exactly the fields this screen needs
    query {
      user(id: "1") {
        name
        orders(last: 3) {
          total
          items { title }
        }
      }
    }

    That single request replaces what might be three REST round trips, and it returns no more than asked. For a big product with mobile and web clients, many teams, and a rich graph of related data, that precision is a real win — no over-fetching, no under-fetching, one typed schema as the contract.

    The honest caveat is operational complexity. You need a schema and resolvers, a strategy for the N+1 query problem (typically a batching layer like DataLoader), and a caching plan — because everything is usually one POST to /graphql, you lose REST's free HTTP caching and lean on client-side normalized caches and persisted queries. GraphQL is powerful, but it's something you *operate*, not something you drop in.

    Developer working with data and code on multiple screens

    Developer working with data and code on multiple screens

    tRPC: delete the boundary when you own both sides

    tRPC's bet is narrower and, under its condition, radical: if the client and server are both TypeScript and the same team owns them, the entire serialization-contract dance is waste. So it lets you define server procedures and call them from the client as if they were local typed functions — the types flow through a single import, with no schema and no code generation.

    ts
    // tRPC — define procedures on the server...
    export const appRouter = router({
      getUser: publicProcedure
        .input(z.object({ id: z.string() }))
        .query(({ input }) => db.user.findUnique({ where: { id: input.id } })),
    
      addUser: publicProcedure
        .input(z.object({ name: z.string() }))
        .mutation(({ input }) => db.user.create({ data: input })),
    });
    
    // ...and call them on the client, fully typed, no codegen
    const user = await trpc.getUser.query({ id: "1" });
    const created = await trpc.addUser.mutate({ name: "Ada" });

    What you get is the tightest developer loop in this comparison: rename a field or change an input on the server and the client lights up with a type error *at build time*, before anything ships. Input validation (via Zod or similar) is built into each procedure, and there's essentially no boilerplate. What you pay is the hard boundary from the intro — it only works inside your TypeScript world. It's the natural fit for a Next.js app, a SaaS, or any starter where one team owns the whole stack, which is exactly why there's a healthy market for tRPC templates and starters.

    Type Safety: The Difference People Feel Day to Day

    This is where the three separate most sharply in daily experience.

  • tRPC gives end-to-end type safety for free — the client and server share types directly, so there's a single source of truth and nothing to regenerate. Change a procedure, and every caller updates or errors immediately.
  • GraphQL gives strong type safety through its schema, but you typically run code generation to turn the schema into TypeScript types for your client. It's excellent once wired up; it's a build step you maintain.
  • REST gives you whatever you enforce. Plain REST has no type contract, so frontend and backend types drift unless you adopt OpenAPI + codegen (or a typed client generator), which brings REST closer to the others at the cost of setup.
  • If a tight, zero-ceremony type loop within one codebase is what you value most, tRPC wins outright. If you need a typed contract that non-TypeScript clients also respect, GraphQL's schema is the stronger model. The reason any of this matters is the same reason TypeScript beats JavaScript for anything you'll maintain: the compiler catching a shape mismatch before runtime is worth a great deal — and your data types usually originate at the database, which is why this pairs so closely with your ORM choice.

    Over-Fetching, Under-Fetching, and Round Trips

    The classic REST pain is fetching a user, then their orders, then each order's items — three requests, and each returns fixed shapes you may not fully use. GraphQL was designed to erase exactly that: one query, the precise nested shape, one round trip. tRPC sits in between — each procedure returns whatever you coded it to return, so you avoid over-fetching by *designing* procedures for your screens, but you don't get GraphQL's arbitrary client-driven field selection.

    The practical read:

  • Many varied clients, big graph, want precise field selection → GraphQL earns its complexity.
  • One client you control, procedures shaped for your screens → tRPC gives you the right data per call without a query language.
  • Public/cacheable resources, simple shapes → REST is fine, and you can add a few purpose-built endpoints for the composite views.
  • Don't adopt GraphQL *solely* to avoid a couple of extra REST calls — that's paying graph-sized complexity to solve an endpoint-sized problem. Adopt it when field-precision across many clients is a recurring, structural need.

    The Next.js App Router Changes The Question

    Here's the shift that catches people out: in the Next.js App Router, a lot of data access moves to the server, *before* any API layer is involved.

    Server Components fetch data on the server — you can call your database or service directly, with full type safety, and render the result into the initial HTML. Mutations run through Server Actions with typed arguments. For a page that displays server data and mutates it with a form, you may not need a client API layer at all.

    Where an explicit API layer still earns its keep is interactive client state and consumers beyond your own render tree:

  • Client Components that call the server after render (search-as-you-type, infinite scroll, live polling)
  • A typed, organized surface your whole client shares, with auth middleware and input validation in one place
  • Anything external: webhooks, mobile apps, partner integrations, public endpoints
  • The common 2026 pattern is both: Server Components and Server Actions for server-side fetching and mutations, plus tRPC for the client-side interactive calls that want a typed, batched, organized API — and a small REST surface for the genuinely public parts (Stripe webhooks, a partner API). They compose rather than compete. If your public surface is the whole point, a dedicated Hono API starter is a clean way to build the REST side.

    Two developers reviewing architecture together at a desk

    Two developers reviewing architecture together at a desk

    Caching: The Quiet Deciding Factor

    Caching is where REST's age becomes an advantage. Cacheable GET URLs get free browser, proxy, and CDN caching — an enormous, low-effort performance and cost win for read-heavy public APIs. That alone keeps REST the right call for many public surfaces.

    GraphQL trades that away: with everything as one POST to /graphql, HTTP caching no longer applies out of the box, so you lean on client-side normalized caches (Apollo, urql, Relay) and server-side strategies, plus persisted queries to make responses cacheable again. It's solvable, but it's work.

    tRPC caches at a different layer — it batches multiple calls into one HTTP request and pairs naturally with a client cache like TanStack Query (which it integrates with directly), giving you deduplication and background refetching on the client. What it doesn't give you is CDN-level caching of a public URL, because it isn't a public URL API.

    If cheap, edge-cacheable reads for the public are central, REST has a structural edge. If your caching lives on the client for an app you own, tRPC's TanStack Query integration is excellent. GraphQL needs the most deliberate caching design of the three.

    Versioning, Evolution, and Breaking Changes

    How each style handles change matters for anything long-lived or consumed by others:

  • REST versions explicitly and bluntly — /v1/, /v2/ — which is clear but means maintaining old versions for consumers you can't force to upgrade.
  • GraphQL favours continuous evolution: add fields freely, deprecate old ones with @deprecated, and avoid hard version bumps. Great for a schema many external clients depend on.
  • tRPC barely has a "versioning" problem *inside* your codebase — because the client imports the server's types, a breaking change is a compile error you fix immediately, not a runtime surprise a consumer discovers later. That's a benefit for a monolith and a non-answer for external consumers (who don't import your types at all).
  • The pattern: for public APIs with consumers you don't control, GraphQL's additive evolution or REST's explicit versions protect them. For an internal TypeScript app, tRPC turns "breaking change management" into "fix the type errors before you ship."

    Bundle Size and Performance: What's Real

    For the client, tRPC and a plain fetch-based REST client are light; GraphQL clients like Apollo add more weight for their normalized cache and features (lighter clients like urql exist). But as with most such comparisons, on a data-heavy app already shipping hundreds of kilobytes, the client-library difference is rarely your bottleneck.

    The performance wins that matter are architectural, not library-branded: round-trip count (GraphQL and well-shaped tRPC procedures cut trips REST might multiply), caching (REST's CDN edge vs client caches), and the N+1 problem (a real GraphQL and ORM concern you solve with batching). Your API latency, database queries, and render tree decide performance far more than whether the wire format is REST, GraphQL, or tRPC.

    Lock-In and Handoff: What Someone Inherits

    If you're building something you'll hand to a client, sell on a marketplace, or leave for a team, your API layer is a big part of what they inherit.

    Clean modern code on a laptop screen

    Clean modern code on a laptop screen

    Portable regardless of choice: your business logic, your database, your components. The data *access* style wraps them; the logic underneath moves with you.

    Where the inheritance gets opinionated:

  • REST is the most universally inheritable — every developer and every stack understands it, and a buyer can consume it from anything. The safest choice for a template whose value is a consumable API.
  • tRPC is a joy to inherit *if the buyer stays in TypeScript and owns both ends* — it reads as modern, tidy, end-to-end-typed code and lowers perceived risk for a self-contained full-stack starter. It's a poor fit to inherit if the buyer needs to expose the API to non-TypeScript consumers.
  • GraphQL signals a serious, flexible data layer for a large product — attractive when the pitch is exactly that, heavier than a small buyer wants when it isn't.
  • The failure mode that actually hurts resale isn't the style — it's incoherence: some routes REST, some ad-hoc fetch calls, no input validation, no consistent auth or error handling, types that lie about the runtime shape. Pick one primary style, validate inputs at the boundary, handle auth and errors consistently, and make the types match reality. A coherent API story is part of what separates a template buyers trust from one they abandon — see what makes code production-ready for the rest of that checklist.

    Which One for Which Project

    Choose tRPC if:

  • You own both ends and both are TypeScript (a Next.js app, a SaaS, a full-stack starter)
  • You want end-to-end type safety with zero codegen and the tightest dev loop
  • Your API is internal to your own frontend, not consumed by third parties
  • You want input validation and organized procedures with minimal boilerplate
  • You're selling a self-contained full-stack template and want modern, tidy, typed code
  • Choose REST if:

  • Your API is public, polyglot, or consumed by clients you don't control
  • You need free HTTP/CDN caching for read-heavy endpoints
  • You want the most universal, lowest-learning-curve contract
  • You're shipping webhooks, a partner API, or anything a mobile app consumes
  • You're selling a template whose value is a consumable backend/API
  • Choose GraphQL if:

  • Many different clients need different slices of a large, interconnected graph
  • Avoiding over- and under-fetching across those clients is a structural, recurring need
  • You have the capacity to operate a schema, resolvers, batching, and caching
  • You want additive schema evolution for external consumers instead of version bumps
  • The product's pitch *is* a flexible data graph for many teams
  • If you're still unsure:

    For a TypeScript app you own end to end — the common case — start with the App Router's Server Components and Server Actions, add tRPC for interactive client calls, and expose a small REST surface only for the genuinely public parts. Reach for GraphQL when field-precision across many clients becomes a real, repeated problem — not before.

    The Bottom Line

    There's no universal winner — there's a right API layer for who consumes it and who inherits it.

  • "TypeScript full-stack app I own end to end" → tRPC
  • "Public, polyglot, or cacheable API" → REST
  • "Large graph, many clients, precise field selection" → GraphQL
  • "Next.js App Router, mostly my own frontend" → Server Components + Server Actions, tRPC for interactive calls
  • "Selling a template that must look production-ready" → tRPC for a self-contained full-stack starter, REST for a consumable API
  • Whichever you choose, three habits outlast the decision: match each consumer to the right tool — tRPC for internal TypeScript traffic, REST for public and polyglot, GraphQL for a large multi-client graph — validate inputs and handle auth and errors at the boundary consistently, and make your types match reality so the contract never lies. Those cost an afternoon and save a month.

    Ready to turn what you build into income? List your template or app on CodeCudos, see how the API layer fits the wider stack in our best tech stack for web apps in 2026 guide, settle the data-fetching question with TanStack Query vs SWR vs RTK Query, or make sure the whole codebase reads as production-ready.

    Frequently asked questions

    Is tRPC a replacement for REST or GraphQL?

    Not in the general case — it solves an overlapping problem under a narrower condition. tRPC gives you end-to-end type safety by letting a TypeScript client call TypeScript server procedures directly, with the types shared through a single import and no schema, code generation, or serialization contract in between. That is a genuine replacement for REST or GraphQL when the same team owns both ends and both are TypeScript, which describes most Next.js apps and SaaS products. But tRPC deliberately is not a public, language-agnostic protocol: a mobile app in Swift, a partner integration in Python, or any third-party consumer cannot 'call your tRPC' the way they call a REST or GraphQL endpoint, because the whole point is that the client imports your server's types. So it does not replace REST or GraphQL for public APIs, polyglot systems, or anything you expose to consumers you do not control. The honest framing is that tRPC and REST/GraphQL answer different questions: tRPC optimizes the internal seam of one TypeScript codebase, while REST and GraphQL are contracts anyone can consume.

    What is the difference between REST and GraphQL?

    REST models your API as resources at URLs — /users, /users/1, /users/1/orders — and you fetch each with an HTTP method (GET, POST, PATCH, DELETE). It is simple, universal, and works with plain HTTP caching, but it tends toward over-fetching (an endpoint returns a fixed shape, often more fields than a given screen needs) and under-fetching (a screen needs data from three resources, so you make three round trips or build a custom endpoint). GraphQL replaces many endpoints with one, where the client sends a query describing exactly the fields and nested relationships it wants, and the server returns precisely that shape in a single request. That solves over- and under-fetching elegantly and is powerful when many different clients need different slices of a large data graph. The trade is complexity: GraphQL needs a schema, resolvers, a plan for the N+1 query problem, and HTTP caching no longer works out of the box because everything is usually one POST to /graphql. REST is simpler and cache-friendly; GraphQL is more flexible and precise. Neither is strictly better — it depends on how many clients you have and how varied their data needs are.

    Is tRPC good for the Next.js App Router?

    Yes, though the App Router changes how much of it you need. In the Pages Router, tRPC was the cleanest way to get typed data from server to client, and it remains excellent for interactive Client Components that call the server — mutations, forms, anything that runs after the initial render. In the App Router, Server Components already fetch data on the server with full type safety (you just call your database or service directly), and Server Actions handle many mutations with typed arguments, so a share of what tRPC used to do is now covered by the framework itself. Where tRPC still earns its place is a typed, organized API surface that both Client Components and external-ish callers in your own app can use consistently — a well-structured router of procedures with input validation, middleware for auth, and one place your whole client calls. Many 2026 Next.js codebases use both: Server Components and Server Actions for server-side fetching and mutations, and tRPC for the client-side interactive calls that want a typed, batched, organized API. It composes cleanly rather than competing.

    Does GraphQL have a performance problem?

    GraphQL does not have an inherent performance problem, but it has two well-known traps that hurt teams who ignore them. The first is the N+1 query problem: a query that asks for a list of items and a nested field on each can trigger one database query per item unless you batch. The standard fix is a batching layer (DataLoader or an equivalent) that collapses those into a single query — mature GraphQL servers handle this, but you have to set it up. The second is caching: because GraphQL typically sends every request as a POST to a single /graphql endpoint, you lose the free HTTP and CDN caching that REST gets from cacheable GET URLs, so you rely on client-side normalized caches (Apollo, urql, Relay) and server-side caching strategies instead. There is also the risk of expensive client queries — a deeply nested query can be costly — which production GraphQL servers guard with query depth limits, complexity analysis, and persisted queries. None of this makes GraphQL slow; it makes GraphQL something you operate rather than something you drop in. REST and tRPC have fewer of these specific traps, which is part of why smaller teams often prefer them.

    Which API layer is best for selling a template or starter kit?

    It depends on what the template is, and buyers judge the API layer as a signal of how production-ready the code is. For a self-contained TypeScript full-stack starter — a SaaS boilerplate, an admin dashboard, a Next.js app someone will fork and extend — tRPC (or the App Router's Server Actions plus a clean typed service layer) is the strongest choice: it demonstrates end-to-end type safety, means a change to the backend surfaces as a client type error rather than a runtime bug, and reads as modern, tidy code a developer can trust. For a template whose whole value proposition is a public or consumable API — a headless backend, an API starter meant to serve mobile apps or third parties — REST is the safer sell because it is universal and every buyer's stack can consume it, and a GraphQL API starter makes sense when the pitch is specifically a flexible data graph for many clients. The failure mode that hurts resale is the same across all three: an incoherent data layer — some routes REST, some ad-hoc fetch calls, no input validation, no auth middleware, types that lie about the runtime shape. Pick one API style, validate inputs, handle errors and auth consistently, and make the types match reality; that coherence is a large part of what separates a template buyers trust from one they abandon.

    Can I use REST, GraphQL, and tRPC together in one app?

    Yes, and plenty of real codebases do — the three are not mutually exclusive, and mixing them deliberately is common and reasonable. A frequent 2026 pattern is tRPC (or Server Actions) for your own frontend's internal calls because it is the fastest, most type-safe way to talk to your own backend, plus a small REST surface for the things that genuinely need to be public: webhooks from Stripe or other providers, a public API for partners, endpoints a mobile app or third party consumes, or anything that must be cacheable at the CDN. Some larger products add GraphQL on top when many clients need flexible access to a shared data graph. The principle is to match each consumer to the right tool rather than forcing one everywhere: internal TypeScript-to-TypeScript traffic wants tRPC's zero-boilerplate type safety, public and polyglot consumers want REST's universality, and a large multi-client graph may want GraphQL's precision. The thing to avoid is accidental mixing — three half-built styles with no clear boundary — versus intentional layering where each style has a defined role.

    Related guides

    Browse Quality-Scored Code

    Every listing on CodeCudos is analyzed for code quality, security, and documentation. Find production-ready components, templates, and apps — or sell your own code and keep 90%.

    Browse Marketplace →