GraphQL vs REST vs tRPC 2026: API Architecture Pick

Photo: pete weiler / Pexels
An API is a contract between two programs, and the style you pick decides how easy that contract is to change, cache, secure, and explain to someone new. REST, GraphQL, and tRPC are the three styles most web teams weigh in 2026. They solve overlapping problems with different trade-offs: REST optimizes for universality, GraphQL for client-driven queries, and tRPC for type safety inside one TypeScript codebase. This comparison covers how each works, where each breaks, and which kind of project each one suits.
The 30-second answer
| Situation | Usual best fit | Main reason |
|---|---|---|
| Public API for outside developers | REST | Every language, tool, and CDN understands it |
| Mobile and web clients with different data needs | GraphQL | Each client asks for exactly its fields |
| Full-stack TypeScript, you own both ends | tRPC | Types flow from server to client without a build step |
| Service-to-service inside a company | REST or gRPC | Simple contracts, strong tooling |
| Simplest thing that works | REST | Fewest new concepts |
If you are unsure, start with REST and add something else when a specific pain justifies it. The rest of this article explains what those pains look like, so you can recognize them early.
How each style works
An analogy helps keep the three apart. Imagine ordering food.
- REST is a restaurant with set menus. Each URL is a dish, and you get the whole plate as the kitchen composed it. If you only wanted the potatoes, you still receive the full plate.
- GraphQL is ordering à la carte through one waiter. You list exactly the items you want in a single order, and the waiter assembles them. The catch is that the waiter may walk to the kitchen separately for each item unless the kitchen is organized to batch the work.
- tRPC is calling a colleague at the next desk by name. There is no menu and no waiter. You call a function that happens to run on the server, and your editor already knows what it will return.
REST: resources and standard verbs
REST (Representational State Transfer) models an API as a set of resources identified by URLs, manipulated with standard HTTP methods. Reading user 123 is a GET to /users/123. Creating a user is a POST to /users. Responses are usually JSON, and the HTTP status code reports success or failure.
GET /api/users list users
GET /api/users/123 fetch one user
POST /api/users create a user
PUT /api/users/123 replace a user
DELETE /api/users/123 remove a user
Because REST rides directly on HTTP, it inherits HTTP's machinery. The caching standard, RFC 9111, describes how caches store responses, noting that caches most commonly store the successful result of a retrieval request (RFC 9111: HTTP Caching). A GET to a stable URL is therefore something browsers, proxies, and content delivery networks already know how to reuse. Our explainers on how HTTP status codes guide browsers and how CDN caching speeds up websites cover that machinery in detail.
The contract is commonly described with OpenAPI, a vendor-neutral description format for HTTP APIs. The specification's latest release at the time of writing is 3.2.1, dated September 10, 2026 (OpenAPI Specification). From such a description, tools can generate documentation, mock servers, and client libraries in many languages.
GraphQL: one endpoint and a query language
GraphQL is a query language for APIs, originally developed at Facebook and now governed by the GraphQL Foundation, a neutral body hosted by the Linux Foundation (GraphQL FAQ). Instead of many URLs, a server exposes a typed schema, usually behind a single endpoint. The client sends a query that names the fields it wants.
query {
user(id: 123) {
name
email
posts(limit: 5) {
title
publishedAt
}
}
}
The response mirrors the shape of the query, and nothing else comes back:
{
"data": {
"user": {
"name": "Alice",
"email": "alice@example.com",
"posts": [
{ "title": "First post", "publishedAt": "2026-05-01" },
{ "title": "Second post", "publishedAt": "2026-05-08" }
]
}
}
}
The HTTP rules are specific. According to the GraphQL documentation, a server operates on a single URL, usually /graphql, must handle POST for queries and mutations, and may also accept GET for queries only. Mutations must use POST. A response body has up to three keys: data, errors, and extensions (Serving GraphQL over HTTP). That single-endpoint design is why GraphQL can fetch a user, their posts, and each post's comments in one round trip.
tRPC: functions instead of URLs
tRPC (TypeScript Remote Procedure Call) describes itself as designed for TypeScript monorepos. Instead of calling URLs, you call functions on the server from the client, for example api.users.getById({ id: 1 }). The HTTP details become part of the function name, so you write getUser(id) instead of GET /users/:id (tRPC concepts).
The vocabulary is small. A procedure is an endpoint and can be a query, a mutation, or a subscription. A router groups procedures under a namespace. Context is data every procedure can reach, such as the session or a database connection. Middleware runs code before and after a procedure. Validation checks that input has the expected shape.
import { z } from 'zod';
import { router, publicProcedure } from './trpc';
export const appRouter = router({
user: router({
getById: publicProcedure
.input(z.object({ id: z.string() }))
.query(({ input }) => db.user.findUnique({ where: { id: input.id } })),
create: publicProcedure
.input(z.object({ name: z.string(), email: z.string().email() }))
.mutation(({ input }) => db.user.create({ data: input })),
}),
});
export type AppRouter = typeof appRouter;
On the client, trpc.user.getById.query({ id: "123" }) is checked against the server's types. If the server changes the return shape, TypeScript flags the client call at edit time. Under the hood it is still HTTP: according to the project's protocol documentation, queries use GET with JSON-encoded input in a query parameter, mutations use POST with the input in the body, and subscriptions run over server-sent events or WebSockets (tRPC HTTP RPC specification). If you want a closer look at the library itself, our explainer on what tRPC is goes into setup and patterns.
Building the same feature three ways
The test case is a profile page that shows a user plus their five most recent posts.
REST usually needs two requests, one for the user and one for the posts, or a custom combined endpoint:
const [user, posts] = await Promise.all([
fetch(`/api/users/${id}`).then(r => r.json()),
fetch(`/api/users/${id}/posts?limit=5`).then(r => r.json()),
]);
The response types are whatever you declared by hand, so they can drift from the real data. Teams that want stronger guarantees generate types from an OpenAPI description.
GraphQL asks for both in one query, and the server resolves each field:
const data = await client.query({
query: gql`
query Profile($id: ID!) {
user(id: $id) { name email posts(limit: 5) { title } }
}
`,
variables: { id },
});
Types come from a code generator that reads the schema and your queries. That is an extra build step, but it produces precise types for exactly the fields each screen requests.
tRPC calls a function:
const profile = await trpc.user.getProfile.query({ id });
There is no generator, because TypeScript infers the type from the server's router. The price is that the client must be able to import the server's types, which in practice means the same repository or a shared package.
Where each style wins
REST wins on reach and simplicity
REST's strongest advantage is that nearly every developer, tool, and platform already understands it. A new teammate can read a URL and guess what it does. Debugging needs nothing more than a browser or curl. Caches and CDNs work with GET requests by default, so a read-heavy public API gets a lot of free performance.
Public APIs follow naturally. Outside developers expect to call your service from any language without installing your toolchain, and REST lets them. Stripe versions its REST API by date. Since the 2024-09-30.acacia release it ships new versions monthly with no breaking changes and issues a major release with breaking changes twice a year. Callers pin a version through the Stripe-Version header, and the documentation lists the current version as 2026-09-30.endive (Stripe API versioning). That model shows how a REST API can evolve without breaking existing integrations, though it takes deliberate engineering.
REST also suits server-to-server integration, simple create-read-update-delete applications, and anything where HTTP caching is a requirement.
GraphQL wins when clients differ
GraphQL's value shows up when several clients want different slices of the same data. A phone screen needs three fields, a desktop dashboard needs thirty, and a partner integration needs a different mix. With REST, you either over-fetch, add endpoints for each variant, or build a backend-for-frontend layer. With GraphQL, each client states its needs, and the server serves all of them from one schema.
It also helps when data is deeply nested and spread across services. A GraphQL layer can sit in front of several backends and present them as one graph, so a client does not need to know which service owns which field.
The typed schema doubles as documentation. Tools can introspect it to offer autocomplete and query builders, and code generators can produce client types.
GitHub is a useful real-world example because it offers both styles. Its GraphQL API enforces explicit limits, which are worth reading as a lesson in what GraphQL operators must manage, covered below.
tRPC wins on speed inside one codebase
tRPC removes whole layers of ceremony when the same team owns the client and the server in TypeScript. There is no schema file to keep in sync, no generator to run, and no hand-written client wrapper. Adding an endpoint is adding a function, and a breaking change turns into a compile error in the client instead of a runtime bug in production.
It also validates input at the boundary. Because procedures declare their input schema, malformed requests are rejected before your logic runs.
For a solo developer or a small team on a full-stack TypeScript framework, that productivity is the main reason tRPC became popular. If you are choosing a framework alongside the API style, see our comparison of Next.js and React for how server components and routing interact with these choices.
Where each style loses
REST: over-fetching, under-fetching, and drift
REST's weaknesses are the mirror image of its strengths.
- Over-fetching. An endpoint returns a fixed shape. If you need only a name, you still receive the full object.
- Under-fetching. If the screen needs a user, their posts, and each post's comments, a naive client makes a request per resource, so the number of round trips grows with the data.
- Endpoint sprawl. Every screen variant tempts someone to add another endpoint.
- Type drift. Without a generated contract, the types on the client are a hope, not a guarantee.
None of these is fatal. Query parameters such as fields and include, compound endpoints, and OpenAPI-generated clients address most of them. But each fix is a convention your team must invent and follow, whereas GraphQL and tRPC make a choice for you.
GraphQL: caching, cost control, and operational weight
GraphQL moves complexity from the client to the server and to operations. Four costs deserve attention.
Caching is harder. HTTP caches are built around GET requests to stable URLs, whereas a GraphQL query is typically sent by POST to a single endpoint. The specification allows GET for queries, which can enable CDN caching, but the documentation warns that long query strings may exceed URL length limits imposed by browsers and CDNs. The common workaround is persisted documents, where the client sends a short identifier instead of the full query text (Serving GraphQL over HTTP).
Resolvers can multiply database calls. This is the N+1 problem. The GraphQL performance guide uses a hero with friends: one request loads the hero, one loads the friends, and then one more request per friend loads that friend's starships. With N friends, that is N additional requests. The remedy is batching, where requests made within a short window are collected and sent as one, using a tool such as Facebook's DataLoader (GraphQL performance guide). Back in the restaurant analogy, this is the waiter making one trip to the kitchen with the whole order instead of one trip per item. A simple illustration is a list of 20 users, each with posts: a naive implementation makes 1 query for the users plus 20 for their posts, or 21, while a batched one makes 2. If those queries hit large tables, database indexes matter far more than the API style.
Clients control the cost of a request. In REST, the server decides what each endpoint does. In GraphQL, the client composes the query, so a malicious or careless client can ask for something expensive. The official security guidance lists several controls: limit query depth, assign cost weights to fields and reject queries above a budget, set timeouts, use persisted queries (an allowlist of known operations), and apply rate limiting. It advises that rate limiting belongs in the business logic layer, because counting requests does not capture the cost of a GraphQL operation. It also notes that disabling introspection is security through obscurity and not sufficient by itself, and that trusted documents suit first-party clients but cannot be used for public APIs, because third-party operations are not known in advance (GraphQL security guidance). Our explainer on how rate limiting protects APIs shows why request counts alone are a weak signal.
Error handling is unconventional. Because one response can contain both data and errors, GraphQL does not map neatly onto HTTP status codes. The specification says a server whose response has a non-null data key should answer with a 2xx status, even if errors are included, because HTTP has no status for partial success. Validation errors that stop execution typically get a 400. Monitoring tools built around 5xx rates can therefore miss problems unless they inspect response bodies.
tRPC: private by design
tRPC's limits follow from its goal:
- TypeScript only. A Python or Swift client cannot consume the inferred types.
- Not a public API format. Outside developers do not want to install your TypeScript types, so a public API still needs REST or GraphQL.
- Coupled client and server. The type-sharing trick assumes a shared repository or package. Teams that deploy client and server independently, with older mobile apps in the wild, must handle version skew themselves.
- Opaque network traffic. Calls are readable only if you know the protocol. Batching, for example, joins procedure names with commas in one URL, such as
/api/trpc/postById,relatedPosts, and a batch with mixed outcomes may return a 207 Multi-Status response (tRPC HTTP RPC specification).
Batching also has a sharp edge. The docs warn that batched URLs can grow long enough to trigger 413, 414, or 404 errors, and recommend the maxURLLength option or a method override to avoid it (tRPC httpBatchLink).
Performance, caching, and type safety side by side
| Aspect | REST | GraphQL | tRPC |
|---|---|---|---|
| Typical request shape | Resource URL and HTTP verb | Query document, usually POST to one endpoint |
Procedure name; GET for queries, POST for mutations |
| Response size | Fixed per endpoint, often more than needed | Exactly the requested fields | Whatever the procedure returns |
| Round trips for nested data | Often several | Usually one | Usually one or batched |
| HTTP and CDN caching | Native for GET |
Possible with GET or persisted queries; harder by default |
Queries use GET, so possible with care |
| Type safety | Manual, or generated from OpenAPI | Generated from the schema | Inferred, no generation step |
| Public API fit | Excellent | Good, with guardrails | Poor |
| Cost-control burden | Low | High | Low to moderate |
One caution about the performance row. Protocol choice rarely decides speed. A slow query, a missing index, or an unbatched resolver costs far more than the difference between JSON over REST and JSON over GraphQL. Fix the data layer before changing API style.
On type safety, the order for a TypeScript codebase is generally tRPC first (inferred, no generator), then GraphQL with code generation (precise, but a build step), then REST with generated OpenAPI clients, then REST with hand-written types. That ranking is a design judgment, not a measurement, and it depends on how disciplined the team is about keeping contracts current.
How GitHub's limits show the cost of flexibility
GitHub offers both REST and GraphQL APIs, and the GraphQL documentation shows how an operator has to defend against expensive queries. The rules are specific:
- The primary rate limit is points-based: standard users get 5,000 points per hour (some app and enterprise tokens get more or less), and the cost of a query is estimated from the number of requests needed to fill each connection, divided by 100, with a minimum of one point.
- A call cannot request more than 500,000 total nodes, and node counts multiply through nested connections.
- Every connection needs a
firstorlastargument between 1 and 100. - Secondary limits include 2,000 points per minute for the GraphQL endpoint, and requests that run longer than 10 seconds time out (GitHub GraphQL limits).
The multiplication rule is easiest to grasp with arithmetic of our own. A query that asks for 100 repositories, 100 issues in each, and 100 comments on each issue implies 100 × 100 × 100 = 1,000,000 nodes, which is twice the 500,000-node cap. The server can reject it before running it, which is exactly the kind of check the GraphQL security guidance recommends. A REST API would never face that question, because each endpoint has a fixed shape. The flexibility that makes GraphQL attractive is the same flexibility that forces this machinery to exist.
Edge cases and exceptions
Public API that must serve mobile too. Nothing forces a single style. A company can expose REST to outside developers and GraphQL to its own clients, with both calling the same services behind the scenes. GitHub publishing both a REST and a GraphQL API shows the two can coexist.
Microservices and gRPC. gRPC is a contract-first remote procedure call framework. You define services and messages in .proto files using Protocol Buffers, and the toolchain generates client and server code. Its documentation has guides for 13 languages, including Go, Java, Python, Rust, and C#, which makes it a strong fit for internal service-to-service calls where all sides share a schema (gRPC introduction). For browsers and public developers, REST or GraphQL remains more common.
Adding GraphQL over existing REST. A GraphQL layer can wrap existing REST endpoints, which lets teams adopt it incrementally for one client while the rest of the system stays unchanged. The cost is that the GraphQL layer now has to batch its REST calls well or it reproduces the N+1 problem across the network.
tRPC with non-TypeScript clients. If a mobile app in Swift or Kotlin joins later, the tRPC types do not help it. Teams in this position often add REST endpoints for the new client or move the shared surface to an OpenAPI-described layer.
Lightweight servers. Framework choice affects how much structure you get. An edge-first framework such as Hono can serve plain REST endpoints, which is often enough for a small service where a heavier API layer would be overkill.
Real-time updates. GraphQL defines subscriptions, and tRPC supports subscriptions over server-sent events or WebSockets. REST has no built-in push mechanism, so teams pair it with WebSockets or server-sent events. All three can do real-time work; the question is how much of the plumbing the framework gives you.
Common mistakes
REST
- Inconsistent URL patterns, such as
/getUsernext to/users/{id}. - Misusing status codes, for example returning 200 with an error message in the body.
- No pagination strategy until a list endpoint becomes slow.
- No versioning plan until the first breaking change.
GraphQL
- Shipping resolvers without batching, which creates N+1 query storms.
- No depth or cost limits on a public endpoint.
- Counting requests for rate limiting instead of measuring query cost.
- Treating schema deprecation as a free pass to remove fields without checking usage.
tRPC
- Using it for a public API.
- Skipping input validation, which throws away one of its main benefits.
- Letting client and server drift apart in deployment, so that an old client calls a new server.
- Not batching related calls, which multiplies requests on a busy page.
What to pick if you are...
A solo developer building a TypeScript product with Next.js. Try tRPC for internal calls. Keep REST routes for anything a third party might call, such as webhooks.
A startup with web, iOS, and Android clients on one backend. GraphQL deserves a serious look, provided you commit to batching, depth limits, and cost controls from day one. If you cannot staff those, a well-designed REST API with a fields parameter is a reasonable alternative.
A team building a public developer platform. REST, described with OpenAPI, with a documented versioning policy. Add GraphQL later if integrators ask for it.
A platform team connecting internal services. REST for simplicity or gRPC for strict contracts and generated clients in many languages.
A team learning web development. Learn REST first, because the concepts (resources, verbs, status codes, caching) carry over to the other two. Our complete web development learning path puts API design in context, and TypeScript for beginners covers the language that tRPC depends on.
The takeaway
The real question behind this comparison is who your API's callers are. If they are strangers, you need a contract that survives without your tooling, and REST has the longest track record there. If they are your own clients with varied needs, GraphQL lets them shape their requests, at the cost of query-cost controls and caching work that you must build and operate. If they are your own code in the same repository, tRPC removes the contract-maintenance step entirely. GitHub shows the middle path: it publishes both a REST and a GraphQL API, and its GraphQL documentation shows how much policy a flexible query language demands. Pick the style that matches who calls you, and spend the first week on the data layer, because that is where most real performance is won or lost.
Frequently asked questions
Q: Is GraphQL replacing REST? A: No. They are different trade-offs, and large organizations run both. GitHub publishes REST and GraphQL APIs side by side, and Stripe's public API remains REST with date-based versioning.
Q: Should a new project use REST or GraphQL? A: Use REST unless you can name the problem GraphQL solves for you, such as multiple clients with different data needs or deeply nested data across services. GraphQL adds caching, cost-control, and batching work that REST does not require.
Q: Can GraphQL use HTTP caching?
A: Partly. The specification lets servers accept GET for queries, which can enable HTTP and CDN caching, but long query strings can exceed URL limits. Persisted documents, which replace the query text with a short identifier, are the usual workaround.
Q: Is tRPC only for Next.js? A: No. It is a TypeScript library, and its documentation describes it as designed for TypeScript monorepos. Next.js is a popular pairing, but the real requirement is that client and server share TypeScript types.
Q: Can I use tRPC for a public API? A: Technically you can expose the HTTP endpoints, but consumers would not get the main benefit, which is shared types, and they would have to learn tRPC's request format. Public APIs are better served by REST or GraphQL.
Q: What is the N+1 problem? A: It is a pattern where loading a list triggers one extra data request per item. A list of 20 users with posts can cause 1 query plus 20, or 21. Batching, for example with DataLoader, collapses the extra requests into one.
Q: What about gRPC? A: gRPC uses Protocol Buffers to define services and generate clients and servers, with guides for 13 languages in its documentation. It suits internal service-to-service calls where both sides can share a schema.
Q: Do API protocols differ much in speed? A: Rarely enough to matter on their own. Database queries, indexing, caching, and network conditions usually dominate. Measure your data layer before blaming the API style.
Sources
- GraphQL: Serving over HTTP, Security, Performance, and FAQ
- GitHub Docs: Rate limits and query limits for the GraphQL API
- tRPC: Concepts, HTTP RPC specification, and httpBatchLink
- RFC 9111: HTTP Caching
- OpenAPI Specification
- Stripe: API versioning
- gRPC: Introduction
Documentation versions and figures were checked in October 2026 and may change.
Follow new articles
Add SciHub101 to your feed reader to see new science and technology stories.
About the Author
SciHub101 is an independent science and technology publication founded and edited by Buğra Sözeri, a software engineer and app developer based in Türkiye. AI tools assist with drafting and research. Source documentation in the existing archive is uneven, and articles are being reviewed and corrected. The Editorial Team byline identifies editorial responsibility; it does not certify specialist or independent human review. Where we compare products, anything built by our own team is labelled as such — see the editorial guidelines and About page for how corrections and disclosures work.
Related Articles

Web Accessibility Guide 2026 — WCAG, ARIA, Testing Tools
Web accessibility for developers: what WCAG 2.2 AA requires, how to use semantic HTML and ARIA, keyboard support, and a testing workflow that catches bugs.

htmx vs React 2026 — When to Skip the JavaScript Framework
htmx vs React in 2026: how server-sent HTML fragments compare with client-rendered JavaScript, where each fits, and what a documented React-to-htmx port saved.

What Is tRPC? End-to-End Typed APIs
tRPC is a TypeScript library for building end-to-end type-safe APIs without code generation, GraphQL schemas, or OpenAPI specs.

FastAPI vs Flask vs Django — Python Frameworks
Compare Django, Flask, and FastAPI on speed, features, async support, and use cases. Code examples and a 10-dimension comparison table.

What Is HTMX? Hypermedia for Modern Web
HTMX explained: how this 14KB library lets you build dynamic web apps with HTML attributes instead of a JavaScript framework, plus how it actually works.

Docker Explained — Containers for Beginners
Learn Docker from scratch: containers vs VMs, Dockerfiles, docker-compose, and how to ship a Node.js app reliably across any environment.