Software / APIs

REST vs GraphQL vs tRPC: Choosing an API Architecture in 2026

/12 min read

REST is a resource-oriented architectural style over HTTP — URLs identify things, verbs do things to them, and caching rides on HTTP semantics; GraphQL is a query language with a single endpoint and a server-held schema where the client specifies exactly which fields it needs; tRPC is a TypeScript-only RPC layer where your server router is the API contract and types flow to the client with zero schema duplication. The short version: public or polyglot API → REST (or GraphQL for complex client-driven reads), TypeScript monolith → tRPC, and combining tRPC internally with a REST or GraphQL facade publicly is a common, sound pattern.

Quick Definitions

Before the trade-offs, a precise framing of each — the debates get muddier than they should because the three solve different problems.

REST

Representational State Transfer, as described by Roy Fielding's 2000 dissertation, is not a protocol but a set of constraints: a uniform interface of resources addressed by URIs, stateless requests, explicit caching semantics, a uniform client-server interface, and optionally a hypermedia constraint (HATEOAS) that in practice almost nobody fully implements — which is fine, as long as you stop calling any HTTP CRUD API “RESTful” and expecting Fielding's blessings. In the field, REST means: JSON over HTTP, resource URLs, GET/POST/PATCH/DELETE, and status codes.

GraphQL

Developed at Facebook in 2012 and open-sourced in 2015, GraphQL is a query language and runtime: clients send a query describing the exact data shape they want, the server resolves it against a schema (types, fields, resolvers), and the response mirrors the request. One endpoint, introspective types, and mutations/subscriptions alongside queries. It was designed to fix mobile overfetching; its second decade has been about taming its operational complexity.

tRPC

Released in 2021 and rapidly adopted in the TypeScript ecosystem, tRPC (typed RPC) lets you define procedures — queries and mutations — in a router on the server, validate inputs with a schema library (usually Zod), and call those procedures from the client as if they were local functions. The server exports a typeof type that the client imports — so input and output types are always in sync with no codegen step. The catch: it is TypeScript-only and assumes client and server ship together.

REST in Practice: Strengths and Friction

// REST: resources over HTTP verbs, URLs as identifiers
GET    /api/orders?status=paid&page=2   // list + filter
GET    /api/orders/ord_10482            // one resource
POST   /api/orders                      // create
PATCH  /api/orders/ord_10482            // partial update
DELETE /api/orders/ord_10482            // delete

// Response: the representation the client asked for
// 200 OK
{
  "id": "ord_10482",
  "status": "paid",
  "totalCents": 4200,
  "items": [{ "sku": "TEE-BLK-M", "qty": 2 }]
}

// Caching comes free with HTTP semantics:
// Cache-Control, ETag, If-None-Match, 304 Not Modified

REST's greatest asset is the caching story: GET requests are cacheable by default, CDNs and HTTP caches understand them natively, and ETag revalidation avoids payload transfer entirely. It is language-agnostic — any client that can speak HTTP can consume a REST API, which makes it the default for public APIs, partner integrations, and mobile backends with diverse consumers.

The frictions are well known. Overfetching — a mobile screen that needs three fields downloads full representations. Underfetching — rendering one screen requires three sequential endpoints and a waterfall. Versioning — breaking changes usually mean a /v2 path or header scheme you maintain forever. And HATEOAS, the hypermedia constraint that was supposed to decouple clients from URL structures, remains a theory almost no production API implements — clients still hard-code endpoints.

GraphQL in Practice: Strengths and Friction

// GraphQL: one endpoint, the client specifies the shape
POST /graphql
{
  order(id: "ord_10482") {
    id
    status
    totalCents
    items { sku qty }
    customer { name email }
  }
}

// Response mirrors the request shape exactly — no more,
// no less. The server resolves each field via the schema,
// and the type system is introspectable by clients and
// tooling (GraphiQL, codegen) for free.

GraphQL's core win is client-driven shaping: the client asks for exactly the fields and depth it needs, eliminating both overfetching and the underfetching waterfall — one round trip can traverse order → customer → orders in a single query. The schema is introspectable, so tooling — GraphiQL/Playwright explorers, TypeScript codegen, persisted-query security — comes almost free. For product-heavy companies with many client teams (web, iOS, Android) constantly reshaping data needs, this flexibility is why GraphQL became the standard at Meta, GitHub, Shopify, Airbnb, and most of the FAANG-scale product orgs.

The costs are equally real. The N+1 problem — resolving a list of 100 rows, each of which triggers a per-item database query — is the classic server-side trap; the standard mitigation is DataLoader (or equivalent batching/caching layers) to coalesce per-row lookups, which every production GraphQL server must implement. Caching is harder: everything is a POST to one URL, so HTTP caching does not apply; you cache at the application layer with query hashing, persisted queries, or a gateway. Complexity and cost control — deeply nested or high-cost queries can hammer the server — require depth limiting, complexity analysis, and rate limits as deliberate design work. And operationally, a GraphQL gateway over many microservices is a significant engineering investment.

tRPC in Practice: Strengths and Friction

// tRPC: the router is the contract; types flow end-to-end
import { initTRPC } from "@trpc/server";
import { z } from "zod";

const t = initTRPC.create();

export const appRouter = t.router({
  order: t.router({
    get: t.procedure
      .input(z.object({ id: z.string() }))
      .query(async ({ input }) => {
        return db.orders.findById(input.id);
      }),
    updateStatus: t.procedure
      .input(z.object({
        id: z.string(),
        status: z.enum(["paid", "shipped"]),
      }))
      .mutation(async ({ input }) => {
        return db.orders.update(input.id, {
          status: input.status,
        });
      }),
  }),
});

export type AppRouter = typeof appRouter;

// Client: import the type, call the procedure.
// Autocomplete, input checking, and return types —
// no codegen, no schema file, no drift.
const order = await trpc.order.get.query({ id: "ord_10482" });

tRPC's win is end-to-end typesafety with zero duplication. In REST or GraphQL, the schema lives in one place and clients regenerate types from it — a step that drifts, breaks CI, and adds toolchain surface. In tRPC, the procedure is the contract: change a Zod input schema on the server and every client call site turns red immediately. Autocomplete on procedures and inputs, typed errors, and inference of return types make the developer experience feel like calling a local module over the network.

The constraints: TypeScript-only on both ends — a Python mobile backend or a third-party integrator cannot consume your tRPC router; shared deployment — client and server types must be released together, which pairs naturally with monorepos and hurts with independently versioned mobile apps; and public API ergonomics — there is no schema document for partners to read, no introspection story for non-TS consumers. tRPC integrates cleanly with TanStack Query (its official adapter maps procedures to query keys, giving you caching and invalidation on top of RPC), which is why it feels so good in React apps — you are effectively getting GraphQL-like ergonomics without the GraphQL server.

Side-by-Side Comparison

DimensionRESTGraphQLtRPC
TypingOptional (OpenAPI)Schema-first, introspectableEnd-to-end TS inference
CachingNative HTTP + CDNApplication-layer onlyVia TanStack Query adapter
ProtocolHTTP verbs + status codesPOST /graphql (HTTP)HTTP + JSON (RPC-style)
Client diversityAny languageAny languageTypeScript only
Over/under-fetchingCommon; needs designSolved by query shapeExact by construction
VersioningPaths or headers (v1/v2)Additive schema evolutionDeploy client + server together
ToolingOpenAPI, Postman, curlGraphiQL, codegen, gatewaysVS Code, TanStack integration
Learning curveLow (conventions vary)Medium–high (server ops)Low (TS + Zod familiarity)
Public API fitExcellentGood (with documentation)Poor

Verdict: REST owns public and polyglot APIs on the strength of HTTP caching and universal client support. GraphQL owns complex, multi-client product reads where query-shape flexibility pays for its operational cost. tRPC owns the TypeScript monorepo, where typesafety and DX beat schema-first ceremony.

Decision Guide: Which Architecture Fits

Reason from your consumers outward:

  • Public API for third parties: REST— or GraphQL if your API is data-graph-shaped (like GitHub's) and consumers benefit from query design. curl-ability, HTTP caching, and language independence dominate. Never tRPC.
  • TypeScript full-stack monorepo (Next.js, SvelteKit, Expo with shared TS): tRPC. The absence of schema duplication and the TanStack Query integration make it the highest-leverage option in this specific context — and this context covers a large share of modern product teams.
  • Multiple client teams (web, iOS, Android) reshaping the same data: GraphQL often wins — one graph, per-client query design, codegen for each platform. This was its founding use case and remains its strongest one.
  • Mobile app with an in-house TS backend team: Depends on release coupling. If the app ships from the same repo and cadence, tRPC works. If App Store review cycles decouple client releases from the server, prefer REST or GraphQL with additive-evolution discipline.
  • CRUD-heavy internal tool: REST or tRPC, whichever your team already knows. The architecture will not decide this project's success.

When to Combine Them

The either/or framing is often wrong in practice. Two common combinations:

  • tRPC internally + REST externally: The product frontend calls tRPC for a fully typed, fast inner loop; a thin REST (or GraphQL) layer exposes a curated, versioned surface to third parties and mobile clients released on independent cadences. You get DX where it matters and a contract where it is required.
  • REST or gRPC between services + GraphQL at the edge: Services communicate with whatever fits (often REST or gRPC); a GraphQL gateway composes them for the client, solving the underfetching waterfall without rewriting the backends.

The failure mode to avoid is running all three for the same consumers with no boundary rationale. Each layer should exist because a concrete consumer need — caching, types, query flexibility — justifies it.

Conclusion

REST, GraphQL, and tRPC are not rivals for one crown — they optimize for different axes. REST optimizes for the network: cacheable, universal, boring in the best way. GraphQL optimizes for the client: exact query shapes across many consumer teams, at the price of server-side complexity discipline. tRPC optimizes for the TypeScript developer: end-to-end inference with no schema duplication, at the price of language and deployment coupling.

In 2026, the healthiest architectures choose by consumer: tRPC in the monorepo, REST at the public boundary, GraphQL where a data graph serves many clients — and nothing at all where a well-designed REST resource would have done.

Key Takeaways

  • REST is resource-oriented HTTP with native caching and universal client support; HATEOAS remains largely theoretical, and versioning via /v2 paths is the usual cost of evolution.
  • GraphQL solves overfetching and underfetching with client-specified query shapes, but brings the N+1 problem (mitigated by DataLoader), application-layer caching, and query cost control as explicit engineering work.
  • tRPC delivers end-to-end TypeScript typesafety with zero schema duplication and pairs with TanStack Query — but is TypeScript-only and assumes client and server deploy together.
  • Decision rule: public or polyglot API → REST (or GraphQL for graph-shaped multi-client reads); TS monorepo → tRPC; never tRPC for third-party consumers.
  • Combining is common and sound: tRPC internally with a REST/GraphQL public facade, or a GraphQL gateway over REST/gRPC services.

FAQ

Is tRPC better than GraphQL?

For a TypeScript monorepo with a single client team, tRPC usually delivers better developer experience — full type inference, no codegen, no schema duplication. GraphQL is better when you have multiple non-TypeScript clients (mobile apps, third-party integrations) that need to shape their own queries against one graph. They solve overlapping problems for different audiences.

Is REST outdated in 2026?

No — it is the default for public APIs, partner integrations, and any situation with diverse or long-lived clients. HTTP-level caching, language independence, and universal tooling keep REST dominant at the API boundary, even as internal application APIs increasingly use GraphQL or tRPC.

Why is GraphQL caching hard?

GraphQL requests are typically POST requests to a single /graphql URL, so HTTP caches and CDNs cannot key on the URL the way they do for REST GETs. Production systems use application-layer caching: hashed or persisted queries as cache keys, response caching at the gateway, and DataLoader-style request batching to reduce database load.

Can tRPC be used with mobile apps?

Only when the mobile client is TypeScript end-to-end (for example, React Native or Expo in the same monorepo as the server). A Kotlin or Swift client cannot consume a tRPC router, and even React Native apps must coordinate deploys with the server since the types are shared artifacts. For independently released native apps, REST or GraphQL is the safer contract.

What is the N+1 problem in GraphQL?

It occurs when resolving a GraphQL query triggers one database (or service) request per item in a list — 100 orders each fetching their customer means 101 queries. The standard fix is DataLoader (or equivalent batching): collect IDs within a single request tick and resolve them in one batched query, with per-request caching to deduplicate repeated keys.

References

  1. Fielding, R. T. "Architectural Styles and the Design of Network-based Software Architectures." UC Irvine Dissertation, 2000. ics.uci.edu/~fielding
  2. GraphQL. "GraphQL Specification." graphql.org. graphql.org/learn
  3. GraphQL Foundation. "GraphQL Security Best Practices." graphql.org/learn/security
  4. tRPC. "tRPC Documentation." trpc.io/docs
  5. React Query. "tRPC + TanStack Query Integration." tanstack.com/query
  6. Shopify Engineering. "GraphQL at Scale." shopify.engineering
Summarize with AI
Page