Skip to main content

πŸ”— GraphQL vs REST

Imagine ordering a burger and being handed the entire kitchen β€” the fries you didn't ask for, plus a drink you have to fetch from a second counter. That's the everyday friction of REST for data-hungry apps. GraphQL rethinks the deal: you write the order, the server assembles exactly that, and it all arrives in one bag.

Week 14 · Wednesday: GraphQL · Lecture 1

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Explain what GraphQL is β€” a query language plus a runtime β€” and how its single endpoint differs from REST's many endpoints
  • Diagnose over-fetching and under-fetching in a REST API and describe how a client-shaped query eliminates both
  • Read a GraphQL schema written in SDL and identify types, fields, non-null !, and lists
  • Weigh the real trade-offs: REST's simplicity and HTTP caching versus GraphQL's flexibility and its caching/complexity costs
  • Choose the right paradigm β€” or a hybrid β€” for a given project

Estimated Time: 55 minutes

Practice: Rewrite a chain of REST calls as a single GraphQL query and sketch its schema.

In This Lesson

What Problem Does GraphQL Solve?

APIs are the glue of modern software: they let a mobile app, a web front end, and a partner's server all talk to the same backend. For two decades, REST has been the default way to design those APIs, and for good reason β€” it's simple, it rides on plain HTTP, and it caches beautifully.

But as front ends grew richer β€” a single profile screen that needs a user, their recent posts, and their followers β€” REST started to strain. You either make three round trips, or you build a bespoke "give me everything for the profile screen" endpoint that no other screen can reuse. GraphQL, created at Facebook in 2012 and open-sourced in 2015, was the answer to exactly this pain: let the client describe the data it needs, and return precisely that β€” no more, no less, in one request.

This lesson is a fair comparison, not a sales pitch. By the end you'll know when GraphQL earns its keep and when REST is still the smarter call.

REST in One Minute

REST (Representational State Transfer) models your data as resources, each with its own URL, manipulated with the standard HTTP verbs. A blog's API might look like this:

GET    /users            # list users
POST   /users            # create a user
GET    /users/123        # read user 123
PUT    /users/123        # replace user 123
DELETE /users/123        # delete user 123
GET    /users/123/posts  # list that user's posts

The shape of each response is fixed by the server. Ask for /users/123 and you get whatever fields the endpoint decided to include β€” every time, for every client. That predictability is a strength for caching and a weakness for flexibility, as we'll see.

graph LR C["Client"] -->|"GET /users/123"| S["REST Server"] S -->|"Fixed user JSON"| C C -->|"GET /users/123/posts"| S S -->|"Fixed posts JSON"| C C -->|"GET /users/123/followers"| S S -->|"Fixed followers JSON"| C

Notice the pattern: one screen, three round trips, three fixed payloads. Hold that picture β€” GraphQL is about to collapse it into one.

What GraphQL Is

GraphQL is two things at once:

  • A query language β€” a small, typed syntax the client uses to describe the data and shape it wants.
  • A runtime β€” server-side machinery that validates a query against a schema and resolves each requested field.

Here's a query that gathers, in a single request, exactly what a profile screen needs:

query {
  user(id: "123") {
    name
    email
    posts(limit: 3) {
      title
      commentCount
    }
    followers(first: 5) {
      name
      avatarUrl
    }
  }
}

The response mirrors the query's shape one-for-one β€” the client never has to guess where a field will land:

Response

{
  "data": {
    "user": {
      "name": "Ada Lovelace",
      "email": "ada@example.com",
      "posts": [
        { "title": "On the Analytical Engine", "commentCount": 12 }
      ],
      "followers": [
        { "name": "Charles B.", "avatarUrl": "/img/cb.png" }
      ]
    }
  }
}

πŸ’‘ The core idea

In REST the server decides the response shape; in GraphQL the client does. GraphQL flips control of the response to the consumer, which is why it shines when many different clients (web, iOS, Android) each want a different slice of the same data.

One Endpoint, Client-Shaped Queries

Perhaps the most visible difference: a GraphQL API exposes a single endpoint β€” conventionally POST /graphql β€” through which every operation flows. There is no /users, no /posts/123/comments; there is just the graph, and your query walks it.

REST exposes many fixed endpoints while GraphQL exposes one endpoint that answers client-shaped queries REST Client /users/123 /123/posts /123/followers 3 round trips, fixed shapes GraphQL Client /graphql one endpoint 1 request, client-shaped
REST spreads data across many endpoints the client must stitch together; GraphQL exposes one endpoint that returns whatever shape the query asks for.

Because the query itself carries the shape, adding a new screen never means adding a new endpoint β€” you just write a new query against the fields that already exist.

Over-fetching & Under-fetching

These two words are the heart of the GraphQL pitch. Learn them and you'll understand 80% of why teams adopt it.

Over-fetching β€” too much data

A REST endpoint returns a fixed payload. If GET /users/123 returns fifteen fields but your list view needs only name and avatarUrl, the other thirteen fields travelled the network for nothing. On a phone with a weak signal, that waste is real.

# GraphQL: ask for exactly two fields, get exactly two fields
query {
  user(id: "123") {
    name
    avatarUrl
  }
}

Under-fetching β€” too few endpoints

The opposite problem: one endpoint can't give you everything, so you make follow-up requests. Fetch the post, then fetch its author, then fetch its comments β€” the classic "waterfall" of round trips that slows a page down. GraphQL resolves related data in the same query, so one request replaces the chain.

graph TD subgraph REST["REST: waterfall of requests"] A1["Client"] -->|"1 - GET post"| B1["Server"] B1 -->|"post"| A1 A1 -->|"2 - GET author"| B1 B1 -->|"author"| A1 A1 -->|"3 - GET comments"| B1 B1 -->|"comments"| A1 end subgraph GQL["GraphQL: one request"] A2["Client"] -->|"query post with author and comments"| B2["Server"] B2 -->|"everything, one response"| A2 end

πŸ” The made-to-order analogy

REST is a diner with a printed menu: you order dish #7 and it arrives exactly as the chef plates it β€” pickles you didn't want included (over-fetching), and if you also want soup you place a second order (under-fetching). GraphQL is a made-to-order counter: you list the ingredients you want, the kitchen assembles that single custom plate, and there's a full catalogue of every ingredient available (the schema).

The Strongly-Typed Schema

None of this magic works without a contract. Every GraphQL API is defined by a schema written in SDL (Schema Definition Language). The schema lists the types, their fields, and the entry points β€” and the runtime validates every incoming query against it before a single resolver runs.

type User {
  id: ID!               # ID! β†’ non-null (the "!" means required)
  name: String!
  email: String         # nullable β€” no "!", so it may be absent
  posts: [Post!]        # a list of non-null Posts
  followers: [User!]
}

type Post {
  id: ID!
  title: String!
  author: User!         # relationships are just typed fields
  comments: [Comment!]
}

# Entry points: every query starts from a field on Query
type Query {
  user(id: ID!): User
  users(limit: Int): [User!]!
  post(id: ID!): Post
}

# Writes live under Mutation
type Mutation {
  createUser(name: String!, email: String!): User!
  deleteUser(id: ID!): Boolean!
}
SDL pieceMeaning
String, Int, Boolean, Float, IDThe five built-in scalar types
!Non-null β€” the field is guaranteed to have a value
[Post!]A list; the inner ! means no null items
type QueryThe root read entry points
type MutationThe root write entry points

βœ… The schema is self-documenting

Because the types are machine-readable, tools can introspect the schema and generate live documentation, autocomplete, and type definitions. There's no separate OpenAPI file to keep in sync β€” the schema is the documentation, and it can never drift out of date.

Trade-offs: Caching & Complexity

GraphQL is not a free upgrade. Its flexibility has a bill, and honest engineers read it before adopting.

Caching gets harder

REST leans on decades of mature HTTP caching: a GET /users/123 has a stable URL, so browsers, CDNs, and proxies can cache it with Cache-Control and ETag headers for free. GraphQL sends everything as a POST to one URL, so that URL-based layer doesn't apply. You cache at a different level instead β€” usually a normalized client cache (Apollo Client, urql) or persisted queries β€” which is more work to set up.

The N+1 problem

A query for 50 posts, each asking for its author, can naΓ―vely fire 1 query for the posts plus 50 queries for the authors β€” the dreaded N+1. It's solvable with batching tools like DataLoader, but it's a footgun REST rarely hands you.

Query cost is unbounded

Because clients compose their own queries, a malicious or careless one can nest deeply and ask the server to do enormous work. Production GraphQL needs depth limiting and query cost analysis β€” guardrails REST doesn't require.

DimensionRESTGraphQL
EndpointsMany, resource-basedOne, /graphql
Response shapeFixed by serverChosen by client
Over/under-fetchingCommonEliminated by design
HTTP cachingExcellent, built-inNeeds custom strategy
Type systemExternal (OpenAPI, optional)Built-in, mandatory
Versioning/v1, /v2Evolve; deprecate fields
Learning curve / opsLowerHigher (N+1, cost limits)

When to Use Which

The right answer is "it depends," and here's what it depends on.

graph TD A["Choosing an API style"] --> B{"Do clients need flexible, varied data?"} B -->|"No, simple CRUD"| C["Lean REST"] B -->|"Yes, many shapes"| D["Lean GraphQL"] C --> E["HTTP caching is critical"] C --> F["File up and downloads"] C --> G["Public API, wide adoption"] D --> H["Mobile, low bandwidth"] D --> I["Aggregating many services"] D --> J["Rapidly changing front ends"]

Reach for REST when

  • Your data maps cleanly onto resources and simple CRUD
  • HTTP caching or a CDN is central to your performance story
  • You're shipping a public API where a low learning curve drives adoption
  • You handle file uploads and downloads (simpler over plain HTTP)

Reach for GraphQL when

  • Many different clients each need a different slice of the same data
  • You're aggregating several backends or microservices behind one graph
  • Bandwidth is precious (mobile) and precise fetching matters
  • Front-end requirements change fast and you want to add fields without new endpoints

πŸ’‘ It's not either/or

Plenty of production systems run both: GraphQL as a flexible aggregation layer for app screens, REST for file endpoints and simple public routes. GitHub, Shopify, and Netflix all expose GraphQL alongside REST. Choosing GraphQL is rarely a decision to delete REST.

Practice & Quiz

πŸ‹οΈ Exercise 1: Collapse the waterfall

Goal: A dashboard currently makes three REST calls to render one card: the post, its author, and its comment count. Write the single GraphQL query that fetches all of it at once, requesting only title and createdAt from the post, name from the author, and the commentCount.

# The REST calls you're replacing:
GET /posts/42
GET /authors/<post.authorId>
GET /posts/42/comments   # only need the count
πŸ’‘ Hint

Start from a root field β€” post(id: "42") β€” then nest the fields you need. Related data like the author is just a nested selection inside the post.

βœ… Solution
query {
  post(id: "42") {
    title
    createdAt
    author {
      name
    }
    commentCount
  }
}

One request, exactly four pieces of data, no over-fetching. The three REST round trips become one.

πŸ‹οΈ Exercise 2: Sketch the schema

Goal: Write the SDL for a minimal Book API: a book has a required id, a required title, an optional year, and a required author (a String). Add a Query with books (a non-null list of non-null books) and book(id: ID!).

βœ… Solution
type Book {
  id: ID!
  title: String!
  year: Int
  author: String!
}

type Query {
  books: [Book!]!
  book(id: ID!): Book
}

Note that book(id: ID!) returns a nullable Book β€” a lookup by id can legitimately find nothing, so it must be allowed to return null.

🎯 Quick Quiz

Question 1: How many HTTP endpoints does a typical GraphQL API expose?

Question 2: Returning fields the client never asked for is called…

Question 3: Which is a genuine advantage of REST over GraphQL?

Best Practices & Pitfalls

βœ… Do

  • Pick the paradigm from the client's needs, not hype β€” flexibility for varied clients favours GraphQL, cacheable resources favour REST
  • Design your GraphQL schema for how the API will be consumed, not around your database tables
  • Plan for the N+1 problem from day one (we'll fix it with DataLoader in a later lesson)
  • Keep REST for file transfer and simple public routes even in a GraphQL shop

❌ Don't

  • Assume GraphQL is "REST but better" β€” you're trading free HTTP caching for flexibility
  • Expose a GraphQL API in production without depth limiting and query cost controls
  • Add a /v2 to a GraphQL API β€” evolve the schema and @deprecated old fields instead
  • Mirror your REST endpoints one-to-one as GraphQL fields; think in terms of the graph of relationships

⚠️ Every field is a resolver

In GraphQL, each field can trigger its own data fetch. A query that looks cheap β€” users { posts { comments { author { name } } } } β€” can explode into thousands of database hits. Always ask "what does the server do to resolve this?" before assuming a query is light.

Summary

πŸŽ‰ Key Takeaways

  • GraphQL is a query language plus a runtime: the client describes the data, the server resolves exactly that
  • One single endpoint replaces REST's many resource URLs, and queries are client-shaped
  • It eliminates over-fetching (too many fields) and under-fetching (too many round trips)
  • A strongly-typed schema in SDL β€” types, fields, non-null !, lists, Query/Mutation β€” is the mandatory contract
  • The trade-off is real: REST is simpler and HTTP-cacheable; GraphQL is flexible but adds caching, N+1, and query-cost concerns

πŸ“š Additional Resources

πŸš€ What's Next?

Theory is settled β€” now let's build one. In the next lesson, Apollo Server setup, you'll stand up a real GraphQL server in Node with type definitions and resolvers, and fire your first query at it from the built-in Apollo Sandbox.

πŸŽ‰ Well compared!

You can now explain, to a skeptical teammate, exactly what GraphQL buys you and what it costs. That judgment is worth more than any single framework.