backend

GraphQL vs REST: Which API Style Should You Use?

Learn the real differences between GraphQL and REST APIs. When GraphQL solves real problems, when REST is the right choice, and how to decide for your project.

By Akash Sharma·6 min read
#graphql
#rest
#api
#backend
#system design
#api design
#web

GraphQL came out of Facebook with big promises: no overfetching, no underfetching, one endpoint for everything. A decade later, most APIs are still REST.

Here's why — and when GraphQL actually wins.

The Problem GraphQL Solves

REST has a specific pain point: the shape of the response is decided by the server.

You have a user profile page. You need: user name, avatar, last 5 posts, and follower count.

In REST, you might hit:

plaintext
GET /users/123              → name, email, bio, phone, address, preferences...
GET /users/123/posts        → all posts with full content, comments, likes...
GET /users/123/followers    → full follower objects...

Three requests. Most of the data you don't need. This is overfetching (too much data) and underfetching (need multiple requests).

In GraphQL, you ask for exactly what you need:

graphql
query UserProfile($userId: ID!) {
  user(id: $userId) {
    name
    avatar
    posts(limit: 5) {
      title
      createdAt
    }
    followerCount
  }
}

One request. Exactly the fields you asked for.

How GraphQL Works

One endpoint (/graphql). Clients send queries (reads) or mutations (writes):

python
# Server: define schema
type_defs = """
  type User {
    id: ID!
    name: String!
    email: String!
    posts: [Post!]!
  }
  
  type Post {
    id: ID!
    title: String!
    content: String!
    author: User!
  }
  
  type Query {
    user(id: ID!): User
    posts(limit: Int): [Post!]!
  }
  
  type Mutation {
    createPost(title: String!, content: String!): Post!
  }
"""
 
# Resolvers — functions that fetch the data
def resolve_user(obj, info, id):
    return db.query("SELECT * FROM users WHERE id = %s", id)
 
def resolve_posts(user, info):
    return db.query("SELECT * FROM posts WHERE user_id = %s", user.id)
javascript
// Client: fetch exactly what you need
const GET_USER = gql`
  query GetUser($id: ID!) {
    user(id: $id) {
      name
      posts {
        title
      }
    }
  }
`;
 
const { data } = useQuery(GET_USER, { variables: { id: "123" } });

Where GraphQL Wins

Multiple clients with different needs. Mobile app needs 3 fields. Web app needs 10. Admin panel needs 20. With REST, you either make a different endpoint for each or overfetch. With GraphQL, each client requests exactly what it needs.

Rapid frontend iteration. Backend schema doesn't change. Frontend devs add or remove fields from their queries without touching the backend. No more "can you add X field to the API?" tickets.

Deeply nested data. Social graphs, content hierarchies, anything where data is connected. REST requires multiple requests. GraphQL traverses the graph in one query.

API for third-party developers. GitHub's GraphQL API lets developers fetch exactly the data they need for their app. GitHub doesn't have to guess what each developer needs.

Where REST Wins

Simplicity. REST is easier to understand, debug, and build. HTTP status codes, standard verbs, curl-able URLs — every developer knows how to use it.

bash
# REST — easy to debug
curl https://api.example.com/users/123
curl -X POST https://api.example.com/orders -d '{"items": [...]}'
 
# GraphQL — harder to curl, need to understand schema first
curl -X POST https://api.example.com/graphql \
  -H "Content-Type: application/json" \
  -d '{"query": "{ user(id: \"123\") { name email } }"}'

Caching is straightforward. REST: GET /products/123 is cacheable by URL. CDN caches it automatically. GraphQL sends everything as POST — cache keys are complex, CDN caching doesn't work out of the box.

File uploads. REST: multipart/form-data upload to /upload. GraphQL: needs workarounds (GraphQL multipart spec or upload endpoint alongside GraphQL).

Simple CRUD APIs. If your API is create/read/update/delete on a few resources, REST is less overhead. GraphQL's schema definition + resolver setup is boilerplate you don't need.

Public APIs that humans debug. REST APIs are self-documenting via their URL structure. GraphQL requires a schema explorer.

The N+1 Problem

GraphQL's biggest pitfall: resolvers run per field, causing N+1 database queries.

python
# GraphQL resolvers
def resolve_posts(obj, info):
    return db.query("SELECT * FROM posts")  # 1 query: fetches 100 posts
 
def resolve_post_author(post, info):
    return db.query("SELECT * FROM users WHERE id = %s", post.user_id)
    # Runs 100 times — one per post = 101 queries total

Fix with DataLoader — batches and deduplicates DB calls:

python
from strawberry.dataloader import DataLoader
 
async def load_users(user_ids: list[int]) -> list[User]:
    # ONE query for all users
    users = db.query("SELECT * FROM users WHERE id = ANY(%s)", user_ids)
    return [users_by_id[uid] for uid in user_ids]
 
user_loader = DataLoader(load_fn=load_users)
 
def resolve_post_author(post, info):
    return user_loader.load(post.user_id)  # Batched, not individual queries

Without DataLoader, GraphQL APIs can be slower than REST despite fetching less data.

Versioning

REST: version in the URL (/v1/users, /v2/users). Clean, explicit, easy to deprecate.

GraphQL: no versioning. You add fields and deprecate old ones in the same schema. Schema evolution instead of API versions.

graphql
type User {
  name: String!
  fullName: String! @deprecated(reason: "Use name instead")
  avatar: String    # New field, clients opt in
}

GraphQL versioning is elegant in theory. In practice, keeping clients updated on deprecated fields requires tooling and discipline.

The Decision

FactorChoose RESTChoose GraphQL
API consumersFew, knownMany, varied
Data shapeSame for all clientsDifferent per client
CachingCriticalLess important
Team familiarityMost teamsGraphQL-experienced team
Real-timePolling / webhooksSubscriptions
Public APIYesDepends

Rule of thumb: REST for server-to-server APIs, internal microservices, and simple CRUD. GraphQL for client-facing APIs with diverse clients (mobile, web, third-party), complex data graphs, and when frontend iteration speed is critical.

Most companies start REST. Facebook, GitHub, Shopify, and Twitter added GraphQL after reaching scale with diverse client needs — not from day one.

Key Takeaways

  • GraphQL: clients request exactly the data they need — solves overfetching and multiple requests
  • REST: simpler, cacheable, familiar — right for most APIs
  • GraphQL wins with multiple clients needing different data shapes, and rapid frontend iteration
  • REST wins with simple CRUD, caching requirements, and public/debuggable APIs
  • N+1 queries are GraphQL's main pitfall — always use DataLoader
  • You can use both: REST for simple endpoints, GraphQL for complex data-fetching needs
  • Don't adopt GraphQL for its own sake — adopt it when REST's shape-constraints become painful

Start REST. Add GraphQL when you have a concrete fetching problem it solves.

Related reading: REST API Design Best Practices · API Gateway Pattern · Rate Limiting Your API

Enjoyed this article?

Get weekly insights on backend architecture, system design, and Go programming.