Architecture

GraphQL vs REST: Choosing the Right API Architecture

Compare GraphQL and REST APIs to make informed decisions for your project.

Plake
Apr 5, 20266 min read1,187 words
GraphQL vs REST: Choosing the Right API Architecture

The GraphQL vs REST debate has been running since 2015 and it isn’t going away — because neither side is universally right. Both architectures solve real problems. The question is which problems you have. This guide maps the genuine trade-offs so you can make the call with your eyes open.

How REST Works

REST (Representational State Transfer) centres on resources — things your system knows about. Each resource has a URL; HTTP methods define what you do to it.

GET    /api/v1/users/42          → fetch a user
GET    /api/v1/users/42/posts    → fetch their posts
POST   /api/v1/posts             → create a post
PATCH  /api/v1/posts/99          → update a post
DELETE /api/v1/posts/99          → delete a post

A typical response:

GET /api/v1/users/42
{
  "id": 42,
  "name": "Alice",
  "email": "alice@example.com",
  "created_at": "2025-01-12T10:00:00Z",
  "plan": "pro",
  "avatar_url": "https://cdn.example.com/avatars/42.webp"
}

The shape of the response is fixed by the server. The client takes all of it, whether it needs it or not.

How GraphQL Works

GraphQL exposes a single endpoint. The client sends a typed query describing exactly what data it needs, and the server returns exactly that — no more.

query GetUserProfile {
  user(id: 42) {
    name
    email
    recentPosts(limit: 3) {
      title
      publishedAt
    }
  }
}

Response:

{
  "data": {
    "user": {
      "name": "Alice",
      "email": "alice@example.com",
      "recentPosts": [
        { "title": "Hello World", "publishedAt": "2026-01-01" },
        { "title": "Second Post", "publishedAt": "2026-01-15" }
      ]
    }
  }
}

The client asked for name, email, and three posts. That’s exactly what came back. No plan, no avatar_url, no extra round-trips.

The Over-Fetching and Under-Fetching Problem

These two failure modes are why GraphQL was built.

Over-fetching — REST returns the full resource. A mobile app rendering just a user’s name and avatar gets every field anyway, burning bandwidth.

Under-fetching — displaying a user profile page in REST often needs multiple sequential requests:

GET /users/42          → get user
GET /users/42/posts    → get their posts
GET /posts/1/comments  → get comments on first post

Three round trips, three waterfalls. GraphQL collapses this into one query:

query DashboardData {
  user(id: 42) {
    name
    avatarUrl
    posts(limit: 5) {
      title
      publishedAt
      commentCount
    }
  }
}

One request. One response. The server fans out to the database; the client waits once.

The N+1 Problem in GraphQL

GraphQL’s power creates its own footgun. If you’re not careful, a query that looks like one database call is actually hundreds:

query {
  posts {
    # 1 query for posts
    author {
      # 1 query per post → N+1
      name
    }
  }
}

The fix is DataLoader (or its equivalent) — a batching and caching layer that collects all the author lookups from a single request tick and fires them as one WHERE id IN (...):

const authorLoader = new DataLoader(async (ids: number[]) => {
  const users = await db.query("SELECT * FROM users WHERE id = ANY($1)", [ids]);
  return ids.map((id) => users.find((u) => u.id === id));
});

REST doesn’t have this problem because the server controls the query. GraphQL gives you the flexibility and the responsibility.

Schema and Type Safety

GraphQL’s schema is a first-class contract between client and server:

type User {
  id: ID!
  name: String!
  email: String!
  posts(limit: Int = 10): [Post!]!
}

type Post {
  id: ID!
  title: String!
  body: String!
  author: User!
  publishedAt: String
}

type Query {
  user(id: ID!): User
  posts(after: String, limit: Int): PostConnection!
}

type Mutation {
  createPost(input: CreatePostInput!): Post!
  deletePost(id: ID!): Boolean!
}

With graphql-codegen, this schema generates TypeScript types for both client and server automatically. A field rename breaks the build — not a runtime 500 in production.

REST achieves similar guarantees with OpenAPI, but the toolchain is less tightly integrated.

Mutations and Subscriptions

Mutations are writes:

mutation PublishPost($id: ID!) {
  publishPost(id: $id) {
    id
    title
    publishedAt
  }
}

Subscriptions push real-time updates over WebSocket:

subscription OnNewComment($postId: ID!) {
  commentAdded(postId: $postId) {
    id
    body
    author {
      name
    }
  }
}

REST handles real-time with Server-Sent Events or WebSockets — possible, but not built into the architecture.

Caching

This is REST’s clearest advantage. HTTP caching is free: ETag, Cache-Control, and CDNs work natively because URLs are stable and GET responses can be cached by a dozen layers between server and browser.

GraphQL uses POST for queries by default (though GET is possible for persisted queries). Most CDN-level caching doesn’t apply. You handle caching at the client (Apollo Client’s normalised cache) or at the field resolver level — it works, but it’s explicit work you have to do.

// Apollo Client normalised cache
const client = new ApolloClient({
  cache: new InMemoryCache({
    typePolicies: {
      Post: {
        keyFields: ["id"],
      },
    },
  }),
});

Error Handling

REST uses HTTP status codes to signal errors. 404 means the resource doesn’t exist; 422 means validation failed; 500 means something broke on the server.

GraphQL always returns 200 OK, even for errors:

{
  "data": { "user": null },
  "errors": [
    {
      "message": "User not found",
      "extensions": { "code": "NOT_FOUND", "id": 99 }
    }
  ]
}

This trips up monitoring tools that alert on non-200 responses. You need explicit error tracking at the application layer.

Tooling Ecosystem

Category REST GraphQL
Docs / testing Postman, Insomnia, Swagger UI GraphiQL, Apollo Sandbox
Code generation OpenAPI Generator graphql-codegen
Client libraries axios, ky, native fetch Apollo Client, urql, TanStack Query
Server frameworks Express, Fastify, Hono Apollo Server, Pothos, Yoga
Caching HTTP-native Apollo Client cache, persisted queries
Type safety OpenAPI + validation Schema-first, fully typed

API architecture comparison illustration

When to Choose REST

  • Public API consumed by third parties (HTTP caching, broad familiarity).
  • Simple CRUD with predictable, stable data shapes.
  • Teams without GraphQL experience — the learning curve is real.
  • When you need aggressive CDN caching for high-read-volume endpoints.
  • Microservice-to-microservice communication.

When to Choose GraphQL

  • Complex product APIs consumed by multiple front-end clients (web, iOS, Android) with different data needs.
  • Mobile apps where bandwidth and battery matter.
  • Rapid iteration — front-end teams can fetch what they need without waiting for a new API endpoint.
  • Highly interconnected data with complex relationship traversals.
  • When a typed, auto-documented schema is important to your workflow.

The Hybrid Approach

In practice, many large systems use both. REST for simple, high-volume, cacheable resources (images, public feeds, webhooks) and GraphQL for authenticated product surfaces with complex data requirements. A BFF (Backend for Frontend) pattern GraphQL layer in front of REST or gRPC microservices is common at scale.

Closing Thoughts

REST is boring and that is a genuine advantage. Its constraints are well understood; its failure modes are documented; every HTTP library speaks it. GraphQL is powerful and that power comes with real operational cost — schema governance, DataLoader wiring, client cache management. Choose the boring tool until the boring tool’s limitations are actually costing you. When they are, GraphQL is ready.

Share:

About the author

The Plake Team

Writes long-form essays on craft, software, and the slow web. Curates the Sunday digest read by 88,000+ readers.

Read more

Keep reading

Related Articles

Back to AllBack to All
RESTful API Design Best Practices

Mar 15, 2026

4 min

RESTful API Design Best Practices

Essential Database Design Patterns for Web Applications

Mar 10, 2026

4 min

Essential Database Design Patterns for Web Applications