Skip to content
fearchitect
State & Data

REST vs GraphQL vs tRPC

Three API styles with distinct fetch, type, and caching trade-offs.

By Abas TurabliReviewed

Summary

REST, GraphQL, and tRPC are three ways to move data between client and server. REST maps operations to HTTP resources and gets free HTTP caching. GraphQL lets clients shape queries but trades that flexibility for N+1 risk and cache complexity. tRPC skips the schema layer entirely — TypeScript types are the contract — but only works inside a monorepo.

Jump to the interview angle

REST vs GraphQL vs tRPC at a glance

DimensionRESTGraphQLtRPC
FetchingFixed shape per endpoint; over/under-fetch is commonClient declares exact fields; one round-trip for nested dataTyped procedure call; shape defined by server return type
TypingManual or OpenAPI-generated; drift is possibleSchema + codegen (e.g. GraphQL Code Generator)Inferred from router — zero codegen, instant type errors
Caching`Cache-Control: s-maxage` on GET; CDN caches by URLPOST bypasses CDN; persisted queries restore GET cachingTanStack Query handles client cache; no HTTP-level caching
Best fitPublic APIs, CDN-cached reads, non-TS consumersDiverse clients needing different field setsFull-stack TypeScript monorepos only
REST uses multiple endpoints with HTTP caching; GraphQL uses one endpoint with client-shaped queries; tRPC shares TypeScript types directly — no schema layer.

tRPC router and typed client call

Define typed procedures on the server; the exported AppRouter type is the sole contract. No schema file, no codegen step.

tRPC router and typed client callts
// server/router.ts
import { initTRPC } from "@trpc/server";
import { z } from "zod";

const t = initTRPC.create();

export const appRouter = t.router({
  post: t.router({
    byId: t.procedure
      .input(z.object({ id: z.string() }))
      .query(async ({ input }) => {
        return db.post.findUnique({ where: { id: input.id } });
      }),
  }),
});

export type AppRouter = typeof appRouter;

// client/PostPage.tsx
import { trpc } from "@/utils/trpc";

export function PostPage({ id }: { id: string }) {
  // Type of data is inferred from the router — no codegen.
  const { data } = trpc.post.byId.useQuery({ id });
  return <h1>{data?.title}</h1>;
}

Change the return shape server-side and the client has a type error immediately — no generated files to sync.

GraphQL caching gap

Queries sent as HTTP POST bypass CDN caches entirely. Use persisted queries to convert them to GET requests — the client sends a hash, the server expands it — restoring URL-based cache hits.

Interview angle

Expect a trade-off question: "when would you pick GraphQL over REST?" Hit over/under-fetching, N+1 with DataLoader, and caching complexity. For tRPC, name the monorepo constraint and zero-codegen benefit.

Soundbite: "GraphQL gives clients control over shape at the cost of HTTP caching; tRPC gives TypeScript control over types at the cost of portability."

Key terms

Over-fetching
Receiving more fields than the client needs from a REST endpoint.
Under-fetching
Needing multiple REST round-trips because one endpoint lacks required data.
N+1 problem
One query for a list plus one query per item — O(N) DB calls instead of two.
DataLoader
Batches and caches per-tick GraphQL resolver calls into a single DB query.
Persisted query
Stores a query by hash server-side so clients send a GET with just the hash.

Further reading

Search fearchitect

Jump to a topic, mode, or action.