03/20/26

GraphQL vs REST APIs

Choosing the right API architecture for your backend

5 Min Read

REST gives you predictable endpoints, HTTP caching, and universal tooling. GraphQL gives you flexible queries, a typed schema, and fewer round trips. The tradeoff is where the complexity lives: REST puts it on the client (multiple requests, over-fetching), GraphQL puts it on the server (resolvers, data loaders, depth limiting).

For most backend services, REST is the simpler and better-supported path. GraphQL earns its place when multiple clients need different views of deeply nested data.

REST

REST APIs are built around resources. Each resource has a URL. You interact with it using HTTP methods:

  • GET /users/42 - retrieve a user
  • POST /users - create a new user
  • PUT /users/42 - replace a user
  • DELETE /users/42 - remove a user

Responses are JSON. Status codes tell you what happened. A typical endpoint:

GET /api/orders/123 { "id": 123, "status": "shipped", "customer_id": 42, "items": [ { "product_id": 7, "name": "Keyboard", "quantity": 1, "price": 89.99 }, { "product_id": 12, "name": "Mouse", "quantity": 1, "price": 49.99 } ], "total": 139.98 }

Why it works: HTTP methods are well-understood. Caching works at every layer - browser, CDN, reverse proxy. Every monitoring tool, HTTP client, and debugging proxy supports REST natively. Each request is self-contained, so horizontal scaling is straightforward.

GraphQL

GraphQL exposes a single endpoint where clients specify exactly what data they need. A typed schema defines all available data and operations:

type User { id: ID! name: String! email: String! orders: [Order!]! } type Query { user(id: ID!): User }

Clients query for specific fields:

query { user(id: "42") { name orders { id status total } } }

Only the requested fields come back. One request can traverse relationships that would take three REST calls.

Why it works: No over-fetching or under-fetching. The schema doubles as documentation. Adding fields is non-breaking since clients only request what they need. Especially valuable when mobile apps, web apps, and third-party consumers all need different views of the same data.

How they compare

DimensionRESTGraphQL
Data fetchingFixed response per endpointClient specifies exact fields
Over/under-fetchingCommon (mitigate with sparse fieldsets)Solved by design
CachingHTTP caching works nativelyRequires application-level caching
Type safetyOpt-in (OpenAPI + codegen)Built into the schema
ToolingUniversalSpecialized (GraphiQL, Apollo Studio)
Server complexityLow (route → handler)High (schema, resolvers, data loaders, depth limits)
PerformancePredictable per endpointVariable (clients can construct expensive queries)
AuthPer-endpointPer-field (more granular, more work)

Caching is REST's biggest structural advantage. Every URL is a cache key. CDNs, browsers, and reverse proxies all work without configuration. GraphQL sends POST requests to a single URL, which breaks URL-based caching entirely. You need Apollo Client's normalized cache, persisted queries, or a CDN layer like Stellate.

Flexibility is GraphQL's biggest structural advantage. A mobile app that only needs a user's name and avatar makes one request and gets exactly that. The same API serves a web dashboard that needs the user's full profile, orders, and payment history - also one request. REST would need separate endpoints or sparse fieldsets for each.

Complexity is the tiebreaker. REST is straightforward to build, monitor, and debug. Each endpoint does one thing, and performance is predictable. GraphQL requires resolvers, data loaders (to avoid N+1 queries), query depth limiting, and field-level authorization. That complexity is worth it when client flexibility is genuinely needed. It's overhead when it's not.

When to use REST

  • CRUD on well-defined resources
  • Caching matters (content delivery, read-heavy workloads)
  • Service-to-service APIs where both sides are under your control
  • Small teams that want to minimize backend complexity
  • Broad compatibility with third-party tools and integrations

When to use GraphQL

  • Multiple clients (web, mobile, third-party) need different views of the same data
  • Deep, interconnected data relationships
  • Public APIs where consumer flexibility is a priority
  • Mobile apps where reducing round trips on poor connections matters
  • Frontend teams that want to iterate on data requirements without backend changes

Building REST APIs with Encore.ts

REST is the right default for most backends. Encore.ts is built around that assumption.

Endpoints are TypeScript functions with typed request and response objects. Encore handles routing, validation, serialization, and documentation:

import { api } from "encore.dev/api"; interface Order { id: number; status: string; items: OrderItem[]; total: number; } export const getOrder = api( { method: "GET", path: "/orders/:id", expose: true }, async ({ id }: { id: number }): Promise<Order> => { return await orderService.findById(id); } );

You get type-safe request validation, automatic API documentation that stays in sync with your code, type-safe service-to-service calls, and built-in distributed tracing - without configuring any of it. The api() function pattern is also straightforward for AI coding agents to follow. Agents generate endpoints matching your existing structure, and TypeScript types ensure the generated code is valid. No separate schema definitions to maintain.

For a complete walkthrough, see How to Build a REST API with TypeScript in 2026. Encore.ts also includes primitives for databases, Pub/Sub, cron jobs, caching, and secrets. When you deploy with Encore Cloud, everything provisions automatically in your own AWS or GCP account.

Ready to escape the maze of complexity?

Encore Cloud is the development platform for building robust type-safe distributed systems with declarative infrastructure.