GraphQL
Understand GraphQL architecture, Schema Definition Language (SDL), queries, mutations, resolvers, HTTP communication, avoiding over-fetching and under-fetching, and key system design trade-offs.
Lesson goal
By the end of this lesson, you will understand how GraphQL operates, how schemas and the Schema Definition Language (SDL) define API contracts, the mechanics of Queries and Mutations over HTTP, how GraphQL eliminates over-fetching and under-fetching, and key architectural trade-offs in system design.
GraphQL is an open-source data query and manipulation language for APIs, as well as a server-side execution runtime for fulfilling queries using a strongly typed schema you define for your data.
GraphQL allows clients to request exact data structures, preventing the receipt of unnecessary data or missing fields
Why GraphQL?
Traditional REST architectures often struggle with client-side data requirements across diverse devices (e.g., mobile apps with low bandwidth vs. desktop dashboards). This leads to two fundamental networking inefficiencies:
1. Over-fetching (Receiving too much data)
- Scenario: Client only needs
usernameandavatarUrl. - REST Behavior:
GET /users/42returns 30+ unneeded fields (address, billing, history). - Result: Wasted bandwidth, higher memory usage, and slower parsing.
2. Under-fetching (Receiving too little data)
- Scenario: Client needs a user profile, 3 latest posts, and comment counts.
- REST Behavior: Requires multiple sequential requests (waterfall round-trips):
GET /users/42(profile)GET /users/42/posts(posts)GET /posts/:id/comments(comments per post)
- Result: High latency, excessive network chattiness, and mobile battery drain.
The GraphQL solution
In GraphQL, a single query asks for the user, their posts, and comment counts simultaneously. The server resolves all nested relationships and returns a single JSON response matching the query structure:
query GetUserProfile {
user(id: "42") {
name
avatarUrl
posts(limit: 3) {
id
title
commentsCount
}
}
}The Defined Schema (SDL)
The backbone of any GraphQL service is its Schema. The schema serves as a strict, type-safe contract between the client and the server, written in Schema Definition Language (SDL).
The schema specifies:
- All available types and their properties.
- Relationships between types (the data graph).
- The root entry points for client operations:
Query,Mutation, andSubscription.
# 1. Custom Object Types
type User {
id: ID!
name: String!
email: String!
role: Role!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String!
published: Boolean!
author: User!
commentsCount: Int!
}
# 2. Enums
enum Role {
ADMIN
AUTHOR
READER
}
# 3. Root Operation Types
type Query {
user(id: ID!): User
feed(limit: Int = 10): [Post!]!
}
type Mutation {
createPost(title: String!, content: String!): Post!
deletePost(id: ID!): Boolean!
}Schema type modifiers
| Syntax | Description |
|---|---|
String | May return a string or null. |
String! | Server guarantees a string value; cannot be null. |
[Post] | The list itself can be null, and items inside can be null. |
[Post!]! | The list is always returned (e.g. []), and every element is a valid Post. |
Core Operations: Queries and Mutations
GraphQL separates operations based on their intent:
| Operation | Primary Purpose | Execution Semantics | HTTP Method |
|---|---|---|---|
query | Read data without side effects | Resolved in parallel by runtime | POST (or GET with caching) |
mutation | Create, update, or delete data (side effects) | Resolved serially (one by one) | POST |
subscription | Stream real-time event updates | Long-lived push connection | WebSockets / SSE |
1. Queries for reading data
A query requests specific fields from objects. Clients pass arguments and dynamic variables directly into the query.
Example: Query with variables
query FetchUserWithPosts($userId: ID!, $postLimit: Int!) {
user(id: $userId) {
name
email
posts(limit: $postLimit) {
title
commentsCount
}
}
}Predictable response shape
Notice how the JSON response mirrored the exact hierarchical structure of the query. What you ask for is exactly what you get—no extra fields, no missing fields.
2. Mutations for updating data
While query fields are executed in parallel by the server runtime, mutations execute serially to prevent race conditions when altering data.
A mutation can modify state on the server and immediately query the updated data in the same round-trip, eliminating the need for a secondary GET request.
mutation CreateNewPost($input: CreatePostInput!) {
createPost(input: $input) {
id
title
published
author {
name
}
}
}How GraphQL Communicates Over HTTP
Although GraphQL is transport-agnostic, the standard practice is to transmit requests over HTTP POST to a single endpoint (e.g., https://api.example.com/graphql).
┌──────────────┐ ┌──────────────┐
│ Client │ ─── POST /graphql (JSON payload) ────> │GraphQL Server│
│ (Web/Mobile) │ <── 200 OK { data, errors } ────────── │ Runtime │
└──────────────┘ └──────────────┘HTTP status codes in GraphQL
Unlike REST—which uses HTTP status codes like 404 Not Found or 422 Unprocessable Entity—GraphQL servers typically return 200 OK as long as the HTTP request was received and processed by the GraphQL engine. If a specific field fails (e.g. database error on posts), GraphQL returns partial data in data and error details in the errors array.
The Execution Model: Resolvers
How does a GraphQL server actually fetch data from databases, microservices, or third-party APIs? Through resolver functions.
Every field in a GraphQL schema is backed by a resolver function on the server. The resolver function tells the runtime how to fetch the data for that field:
┌───────────────────────┐
│ Incoming Query │
└───────────┬───────────┘
│
┌─────────▼─────────┐
│ Query.user │ ──> resolver: db.users.findById(id)
└─────────┬─────────┘
│
┌─────────────┴─────────────┐
▼ ▼
┌───────────────────┐ ┌───────────────────┐
│ User.name │ │ User.posts │ ──> resolver: db.posts.byUser(parent.id)
│ (trivial getter) │ └─────────┬─────────┘
└───────────────────┘ │
┌─────────┴─────────┐
▼ ▼
┌───────────────────┐ ┌───────────────────┐
│ Post.title │ │Post.commentsCount │ ──> resolver: count(post.id)
└───────────────────┘ └───────────────────┘System Design Trade-Offs: When to Use GraphQL
While GraphQL solves over-fetching and simplifies complex client-driven UI requirements, it introduces specific architectural challenges.
┌─────────────────────────────────────────────────────────────────────────────┐
│ GraphQL Architectural Matrix │
├──────────────────────────────────────┬──────────────────────────────────────┤
│ Advantages │ Challenges & Engineering Trade-Offs │
├──────────────────────────────────────┼──────────────────────────────────────┤
│ ✔ Zero over/under-fetching │ ✖ Complex caching (no URL-based CDN) │
│ ✔ Single network round-trip │ ✖ Vulnerable to N+1 backend queries │
│ ✔ Strong contract (SDL & introspection) ✖ Risk of recursive/expensive queries│
│ ✔ Client-driven schema evolution │ ✖ Additional runtime complexity │
└──────────────────────────────────────┴──────────────────────────────────────┘REST vs. GraphQL Feature Comparison
| Feature | REST | GraphQL |
|---|---|---|
| Architecture | Resource-driven, multiple endpoints | Schema-driven, single endpoint (/graphql) |
| Data Fetching | Fixed payloads; prone to over/under-fetching | Exact fields requested by client |
| Round Trips | Multiple waterfall requests for nested data | Single round trip for connected data graph |
| Type Safety | Optional (OpenAPI/Swagger specs) | Mandatory built-in type system (SDL) |
| HTTP Methods | Standard semantics (GET, POST, PUT, DELETE) | Primarily POST (queries & mutations) |
| HTTP Status Codes | Meaningful codes (200, 201, 400, 404, 500) | Typically 200 OK with errors in response body |
| Network Caching | Native HTTP & CDN edge caching via URIs | Requires Persisted Queries or normalized cache |
| Best Suited For | Simple CRUD, file streaming, public caching | Complex UIs, mobile apps, aggregated microservices |
Key Takeaways
- Structured Queries: GraphQL allows clients to request exactly what they need, eliminating over-fetching (wasted payload) and under-fetching (multiple network round trips).
- Defined Schema: The Schema Definition Language (SDL) serves as the type-safe contract defining types, relationships, and entry points (
Query,Mutation,Subscription). - Queries vs. Mutations: Queries are side-effect-free and run in parallel; mutations perform state changes and execute serially.