Skip to content

tRPC vs REST vs GraphQL — API Architecture Comparison

DodaTech 4 min read

In this tutorial, you'll learn about trpc vs rest vs GraphQL. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

tRPC gives end-to-end type safety between server and client over HTTP, REST provides resource-oriented simplicity with universal caching, and GraphQL enables flexible client-driven data fetching — three API approaches for different needs.

At a Glance

Feature tRPC REST GraphQL
Type safety Full (end-to-end) Manual Via codegen
Learning curve Low (just functions) Low Medium
Caching HTTP (limited) HTTP (excellent) Client (Apollo/URQL)
Bundle size Zero (tree-shaken) Zero ~25KB+ (client lib)
Network requests One per procedure One per resource One per query
Over-fetching None (by design) Common None
Tooling ecosystem Minimal Massive Large
IDEs/Playground None Swagger/Postman GraphiQL/Playground
Browsable API No Yes (with docs) Yes

Key Differences

  • Type safety: tRPC infers types from your backend functions directly into the client. No Code Generation. No duplication. REST requires manual type sharing. GraphQL needs codegen tools like GraphQL Code Generator.
  • Caching: REST has excellent HTTP caching support via Cache-Control, ETags, and CDNs. GraphQL typically uses client-side normalized caches (Apollo). tRPC relies on HTTP caching or manual React Query integration.
  • Network efficiency: GraphQL and tRPC send fewer requests because one query can fetch everything needed. REST requires multiple round trips for related resources.
  • Ecosystem maturity: REST has the largest ecosystem — every tool, proxy, and framework supports it. GraphQL has strong tooling (Apollo, Relay, Hasura). tRPC is TypeScript-only and relatively new.

tRPC

// server/router.ts
import { z } from "zod";
import { publicProcedure, router } from "./trpc";

export const threatRouter = router({
  getById: publicProcedure
    .input(z.string())
    .query(async (opts) => {
      const threat = await db.threat.findUnique({
        where: { id: opts.input },
        include: { reports: true, analyst: true },
      });
      return threat;
    }),
});

// client/component.tsx
import { trpc } from "./trpc";

function ThreatDetail({ id }: { id: string }) {
  const { data } = trpc.threat.getById.useQuery(id);
  return <div>{data?.name} - {data?.analyst.name}</div>;
}

REST

// client/fetch.ts
async function getThreat(id: string) {
  const threat = await fetch(`/api/threats/${id}`).then(r => r.json());
  const reports = await fetch(`/api/threats/${id}/reports`).then(r => r.json());
  const analyst = await fetch(`/api/analysts/${threat.analystId}`).then(r => r.json());
  return { ...threat, reports, analyst };
}

GraphQL

query GetThreat($id: ID!) {
  threat(id: $id) {
    name
    severity
    reports { title }
    analyst { name }
  }
}
const { data } = useQuery(GET_THREAT, { variables: { id } });

Expected output (all three):

Threat: "Emotet", Severity: "critical"
Reports: ["Analysis Report #1", "IOC Report"]
Analyst: "Dr. Smith"

Side by Side: Mutation

tRPC

// server/router.ts
export const threatRouter = router({
  create: publicProcedure
    .input(z.object({ name: z.string(), severity: z.enum(["low","high"]) }))
    .mutation(async (opts) => {
      return db.threat.create({ data: opts.input });
    }),
});

// client
const mutation = trpc.threat.create.useMutation();
mutation.mutate({ name: "New Threat", severity: "high" });

REST

await fetch("/api/threats", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "New Threat", severity: "high" }),
});

GraphQL

mutation CreateThreat($name: String!, $severity: Severity!) {
  createThreat(name: $name, severity: $severity) { id }
}
flowchart TD
    A["Choose API Architecture"] --> B{"TypeScript\nfull-stack?"}
    B -->|Yes| C{"Need public\nAPI?"}
    B -->|No| F["REST or GraphQL"]
    C -->|Yes| D{"Flexible data\nfetching needed?"}
    C -->|No| E["tRPC\nE2E type safety\nZero boilerplate"]
    D -->|Yes| G["GraphQL\nClient-driven queries\nSchema-first"]
    D -->|No| F["REST\nUniversal caching\nTooling maturity"]
    style E fill:#bbf7d0,stroke:#16a34a
    style F fill:#dbeafe,stroke:#2563eb
    style G fill:#fef3c7,stroke:#d97706

FAQ

What is tRPC best used for?

tRPC is best for full-stack TypeScript applications where you control both client and server. It eliminates API boilerplate and provides end-to-end type safety without codegen. It is less suitable for public APIs consumed by non-TypeScript clients.

{{< faq "When should I choose REST over tRPC or GraphQL?">}} Choose REST for public APIs consumed by diverse clients (mobile, web, third-party), when you need universal HTTP caching, or when your team spans multiple languages. {{< /faq >}}

Can I use tRPC with React Native?

Yes — tRPC works with React Native via @trpc/client. The same type-safe API calls work on web and mobile with no extra configuration.

How does tRPC handle file uploads?

tRPC can accept FormData for file uploads using the formData input parser, or you can use a separate file upload endpoint and pass the URL to a tRPC mutation.

REST vs GraphQL — GraphQL vs REST — REST vs gRPC — tRPC vs REST


Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro. Updated 2026-06-24.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro