Stockio

REST vs GraphQL vs tRPC: Choosing an API Style

Compare REST, GraphQL, and tRPC through client needs, caching, security, and deployment constraints, then validate your choice with a production-shaped prototype.

By Chidi Nwosu

Backend Engineer ·

On this page

Choosing an API style is mostly a decision about who consumes your service and how independently those consumers evolve. REST, GraphQL, and tRPC can all support reliable products. The expensive mistakes happen when a team chooses one for its syntax while ignoring ownership, compatibility, authorization, and operational work.

Start with consumers and measurable requirements

List your actual consumers: a first-party dashboard, native applications, integration partners, background workers, or unknown developers. Then write down which teams own them and whether releases happen together. Client independence often matters more than whether the transport looks elegant in a component.

Use the API Latency Tester to inspect reachable, non-sensitive test endpoints for response time and status. Compare equivalent requests, not a tiny REST response against a GraphQL request loading an entire dashboard. Browser measurements include network conditions and tool limitations; they are observations, not backend execution profiles.

Define a representative screen before benchmarking. For an order detail page, include the order, customer summary, shipment, and permissions. Record request count, transferred bytes, server duration, and database queries. A single network request can still trigger dozens of database calls. Review broader backend API guides when turning these measurements into service-level expectations.

Also identify what cannot change: existing partner contracts, mobile versions that remain installed for months, or infrastructure that requires standard HTTP caching. These constraints narrow the choice before any framework comparison begins.

Choose REST for explicit resources and broad interoperability

REST-style HTTP APIs work especially well when resources and operations are understandable outside your codebase. An integration developer can inspect a URL, method, status, and JSON body without adopting your frontend language or build system. Resource boundaries provide a useful starting point for documentation and ownership.

For example, retrieving an order can use a stable resource URL, while creating a refund can create a subordinate refund resource. Keep semantics predictable rather than forcing every business action into a misleading update operation.

Use status codes consistently and distinguish authentication failure, forbidden access, missing resources, validation errors, and temporary failures. The MDN HTTP reference explains the underlying method and cache semantics. An OpenAPI contract can support generated clients without tying consumers to a TypeScript server implementation.

HTTP caching is a practical advantage, but not automatic. Configure cache directives, validators, and authorization-aware behavior deliberately. Personalized responses must not accidentally enter a shared public cache. Avoid treating GET as permission to cache everything forever.

The main tradeoff is aggregation. A complex screen may need several resources or a purpose-built endpoint. That is not inherently a failure: a documented dashboard endpoint can be simpler than introducing an entire query runtime. Prefer it when the view is stable and the number of consumers is small.

Choose GraphQL for varied, evolving data requirements

GraphQL gives clients a typed schema and lets them select fields and traverse exposed relationships. This becomes valuable when several clients need different projections of the same domain. A mobile detail view and a desktop operations screen can ask for different shapes without demanding a new endpoint for every variation.

query OrderSummary($id: ID!) {
order(id: $id) {
  id
  status
  customer { displayName }
  shipments { trackingStatus }
}
}

The GraphQL documentation describes this schema-and-operation model. Schema evolution usually means adding fields and deprecating old ones while tracking real usage. Removing a field remains a breaking change, even when the server still has only one URL.

Resolvers need explicit authorization and efficient data access. Without batching or carefully designed queries, a relationship field can create an N+1 query problem. Instrument resolver work and database activity rather than assuming one HTTP request means one database operation.

Client flexibility also creates resource-control work. Apply pagination bounds, operation complexity controls, timeouts, and appropriate rate limits. Persisted or allowlisted operations can help constrain first-party workloads. GraphQL is not intrinsically insecure, but its query surface needs a different abuse model from a fixed endpoint.

Caching is still possible through normalized client caches and carefully configured HTTP strategies. However, it requires more design than simply putting every POST response behind a CDN.

Choose tRPC for a coordinated TypeScript boundary

tRPC is especially attractive when a TypeScript team owns both server and client and wants inferred procedure inputs and outputs. Instead of maintaining a separate client contract by hand, the client derives types from the server router. End-to-end types reduce integration friction during frequent product changes.

The tRPC documentation explains routers, procedures, adapters, and input validation. Keep the distinction clear: TypeScript checks developers' code, while runtime validators check incoming data. A caller can bypass your generated client and send arbitrary requests.

This approach fits a dashboard and its backend particularly well when they share release coordination. It is less natural as the sole public contract for partners using Java, Python, or tools that expect OpenAPI. Native clients and independently deployed consumers deserve an explicit compatibility strategy too.

Avoid importing server runtime dependencies into browser bundles. Share type information through the recommended type-only boundary, and verify what the production bundler actually emits. Treat authentication context, authorization, errors, and pagination as API design work rather than features automatically solved by inference.

Deployment coupling is the central tradeoff. Types can prove that today's source trees agree while yesterday's deployed client still calls an older procedure. Additive changes, compatibility tests, and gradual rollouts remain necessary.

Prototype the operational contract before committing

Build one read path and one write path using the leading candidate. Include a realistic authorization rule, an invalid request, a database timeout, and a stale client. Test the failure paths as carefully as the happy path.

A minimal HTTP read request makes a useful baseline:

const response = await fetch("/api/orders/ord_123", {
headers: { Accept: "application/json" },
});
if (!response.ok) {
throw new Error("Order request failed");
}
const order = await response.json();

Measure operational cost as well as latency: can support staff trace an operation, can you identify expensive consumers, and can you roll back safely? Use correlation identifiers without logging secrets. Inspect representative response shapes with the JSON Formatter, using synthetic data rather than production customer records.

Choose REST for a broad integration surface, GraphQL for meaningful client-driven data variation, and tRPC for closely coordinated TypeScript applications. These are defaults, not laws. A product can expose REST externally and use tRPC internally, provided both paths reuse business rules and authorization instead of creating conflicting implementations.

Document the decision, rejected alternatives, and triggers for revisiting it. This keeps a reversible engineering choice from becoming a permanent ideological commitment.

Key takeaways

  • Choose around consumers, compatibility, and ownership before syntax.
  • REST favors interoperable contracts and familiar HTTP operations.
  • GraphQL supports varied projections but needs query-cost controls.
  • tRPC improves TypeScript coordination without replacing runtime validation.
  • Benchmark equivalent workloads and test failures before standardizing.
Featured sponsor

Reach designers & developers at work

Put your product next to the tools people use every day. One sponsor per placement, no tracking pixels.

Become a sponsor

Keep reading

Related articles

More in Backend & APIs
Backend & APIs

12 Best Free Public APIs for Side Projects

Find approachable public APIs for weather, books, countries, and sample data, plus practical notes on access, limits, and choosing an endpoint for a side project.

Includes: API latencyChidi NwosuChidi Nwosu
Backend & APIs

10 Best Backend-as-a-Service (BaaS) Platforms

Compare ten backend services for app data, sign-in, and server functions, with plain-language strengths, trade-offs, and advice for choosing a platform that fits your project.

Includes: API latencyChidi NwosuChidi Nwosu
Backend & APIs

ETag Caching for APIs: Cache-Control and Redis

Build a safe API caching policy by separating HTTP freshness, conditional ETag validation, and Redis data reuse, with explicit rules for privacy and invalidation.

Includes: API latencyChidi NwosuChidi Nwosu
Backend & APIs

CSRF Protection With SameSite Cookies, Tokens and Origins

Protect cookie-authenticated API mutations with CSRF tokens, origin validation, SameSite cookies, and clear middleware rules that survive real browser login flows.

Includes: API latencyChidi NwosuChidi Nwosu
Backend & APIs

How to Fix CORS Preflight Errors in Your API

Diagnose CORS preflight failures with explicit origins, credential rules, OPTIONS responses, and cache behavior while keeping authentication and authorization separate.

Includes: API latencyChidi NwosuChidi Nwosu
Backend & APIs

API Rate Limiting With Token Buckets and Redis

Design API rate limits with token buckets, atomic Redis operations, fair identities, and Retry-After responses without confusing quotas with concurrency controls.

Includes: API latencyChidi NwosuChidi Nwosu