GraphQL — The Complete Guide
A deep dive into GraphQL: what it is, when & where to use it, how it compares to REST, with real JavaScript examples and diagrams.
🍽️ The Restaurant Analogy
Imagine you walk into a restaurant.
REST API = A Fixed Menu
Every dish comes exactly as described on the menu. If you order a "Burger Combo" you get:
- Burger ✅
- Fries ✅ (even if you don't want them)
- Drink ✅ (even if you have one)
Want extra cheese? You need to call a separate endpoint (waiter). Want the burger without the bun? Too bad — you get what's on the menu.
GraphQL = A Chef's Table (Custom Orders)
You tell the chef exactly what you want:
"I want the burger patty, only cheddar cheese, and the lettuce — skip the bun, fries, and drink."
The chef brings you exactly that. No waste, no extra trips.
🌐 What is GraphQL?
GraphQL is a query language for your API and a runtime for executing those queries. It was created by Facebook in 2012, open-sourced in 2015.
Key ideas:
- Single endpoint (
/graphql) for all operations - Client specifies exactly what data it needs
- Strongly-typed schema defines all possible data shapes
- Supports Queries (read), Mutations (write), Subscriptions (real-time)
🆚 GraphQL vs REST — Side-by-Side Comparison
| Feature | REST API | GraphQL |
|---|---|---|
| Endpoints | Multiple (/users, /posts, /comments) | Single (/graphql) |
| Data fetching | Fixed response shape | Client defines shape |
| Over-fetching | ✅ Common — get unused fields | ❌ Eliminated |
| Under-fetching | ✅ Common — need multiple requests | ❌ Eliminated |
| Versioning | /v1/users, /v2/users | Schema evolution (no versioning) |
| Type system | Optional (OpenAPI/Swagger) | Built-in, mandatory |
| Real-time | Polling / WebSockets (manual) | Native Subscriptions |
| Caching | HTTP caching (GET requests) | Requires custom caching (e.g. Apollo) |
| Learning curve | Low | Medium |
| Tooling | Mature | Growing (GraphiQL, Apollo, etc.) |
| Best for | Simple CRUD, public APIs | Complex data, multiple clients |
🗺️ Architecture Diagram
REST API Architecture
─────────────────────────────────────────────────────────────
Client Server Database
────── ────── ────────
GET /users ────────────► /users handler ────────► SELECT *
GET /posts ────────────► /posts handler ────────► SELECT *
GET /comments ─────────► /comments handler ──────► SELECT *
│
└─ 3 round trips, over-fetched data 😞
GraphQL Architecture
─────────────────────────────────────────────────────────────
Client Server Database
────── ────── ────────
┌──────────────────┐
│ GraphQL Engine │
POST /graphql ────────────►│ │
{ users { name } } │ Schema + Types │──► Resolvers ──► DB
{ posts { title } } │ │
└──────────────────┘
│
└─ 1 round trip, exact data needed ✅
Over-fetching vs Under-fetching (REST Problem)
─────────────────────────────────────────────────────────────
Need: user name only REST Returns:
┌──────────────────┐ ┌──────────────────────────┐
│ { name } │ vs │ { id, name, email, │
└──────────────────┘ │ address, phone, │
│ createdAt, updatedAt, │
│ preferences, ... │
│ } ← 90% wasted 🗑️ │
└──────────────────────────┘
Under-fetching (REST Problem):
┌─────────────────────────────────────────────────────────┐
│ Want: Post + Author + Comments │
│ Must call: GET /post/1 → GET /users/42 → GET /comments │
│ 3 requests → 3 round trips 😞 │
└─────────────────────────────────────────────────────────┘
GraphQL Solution (1 request):
┌─────────────────────────────────────────────────────────┐
│ query { │
│ post(id: 1) { │
│ title │
│ author { name } │
│ comments { text } │
│ } │
│ } │
└─────────────────────────────────────────────────────────┘📦 GraphQL Core Concepts
1. Schema (The Contract)
The schema defines all possible data and operations. It's the "menu" of everything the API can do.
# Type definitions
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
body: String!
author: User!
comments: [Comment!]!
}
type Comment {
id: ID!
text: String!
author: User!
}
# Root types
type Query {
user(id: ID!): User
users: [User!]!
post(id: ID!): Post
}
type Mutation {
createUser(name: String!, email: String!): User!
createPost(title: String!, body: String!, authorId: ID!): Post!
}
type Subscription {
newPost: Post!
}2. Queries (Read Data)
# Ask for exactly what you need
query GetUser {
user(id: "1") {
name # ← only name, not all fields
posts {
title # ← only title from posts
}
}
}3. Mutations (Write Data)
mutation CreateUser {
createUser(name: "Alice", email: "alice@example.com") {
id
name
}
}4. Subscriptions (Real-time)
subscription OnNewPost {
newPost {
title
author {
name
}
}
}💻 JavaScript Code Examples
Example 1: REST API — Traditional Approach
// ❌ REST: Multiple endpoints, over-fetching, under-fetching
const BASE_URL = "https://jsonplaceholder.typicode.com";
// Scenario: Show blog post with author name and comment count
// Problem: Needs 3 separate API calls!
async function getPostDataWithREST(postId) {
try {
// Round trip 1: Get post
const postRes = await fetch(`${BASE_URL}/posts/${postId}`);
const post = await postRes.json();
// Returns: { id, userId, title, body } — we only need title & userId
// Round trip 2: Get author (using userId from post)
const userRes = await fetch(`${BASE_URL}/users/${post.userId}`);
const user = await userRes.json();
// Returns: { id, name, username, email, address, phone, website, company }
// We only need: name — rest is wasted bandwidth!
// Round trip 3: Get comments
const commentsRes = await fetch(`${BASE_URL}/posts/${postId}/comments`);
const comments = await commentsRes.json();
// Returns full comment objects — we only need the count!
return {
title: post.title,
authorName: user.name,
commentCount: comments.length,
};
} catch (err) {
console.error("REST Error:", err);
}
}
// Usage
getPostDataWithREST(1).then(console.log);
// Output: { title: '...', authorName: 'Leanne Graham', commentCount: 5 }
// BUT: 3 HTTP requests made! Over-fetched on every call.Example 2: GraphQL Server Setup (Node.js + Apollo Server)
// ✅ GraphQL Server: schema + resolvers + single endpoint
// Install: npm install @apollo/server graphql
import { ApolloServer } from "@apollo/server";
import { startStandaloneServer } from "@apollo/server/standalone";
// ─── In-memory "database" ───────────────────────────────────────────────────
const users = [
{ id: "1", name: "Alice Johnson", email: "alice@example.com" },
{ id: "2", name: "Bob Smith", email: "bob@example.com" },
];
const posts = [
{
id: "1",
title: "GraphQL is Amazing",
body: "Here is why...",
authorId: "1",
},
{ id: "2", title: "REST vs GraphQL", body: "Comparison...", authorId: "1" },
{ id: "3", title: "Node.js Tips", body: "Pro tips...", authorId: "2" },
];
const comments = [
{ id: "1", text: "Great post!", postId: "1", authorId: "2" },
{ id: "2", text: "Very helpful", postId: "1", authorId: "2" },
{ id: "3", text: "Loved this!", postId: "2", authorId: "1" },
];
// ─── Schema (Type Definitions) ───────────────────────────────────────────────
const typeDefs = `#graphql
type User {
id: ID!
name: String!
email: String!
posts: [Post!]! # Nested relationship
postCount: Int!
}
type Post {
id: ID!
title: String!
body: String!
author: User! # Nested relationship
comments: [Comment!]!
commentCount: Int!
}
type Comment {
id: ID!
text: String!
author: User!
post: Post!
}
type Query {
# Get all users
users: [User!]!
# Get a single user by ID
user(id: ID!): User
# Get all posts
posts: [Post!]!
# Get a single post
post(id: ID!): Post
}
type Mutation {
createUser(name: String!, email: String!): User!
createPost(title: String!, body: String!, authorId: ID!): Post!
addComment(text: String!, postId: ID!, authorId: ID!): Comment!
}
`;
// ─── Resolvers (Logic behind each field) ─────────────────────────────────────
const resolvers = {
Query: {
// Return all users
users: () => users,
// Return user by ID
user: (_, { id }) => users.find((u) => u.id === id),
// Return all posts
posts: () => posts,
// Return post by ID
post: (_, { id }) => posts.find((p) => p.id === id),
},
// Nested resolvers — only called if client requests these fields!
User: {
posts: (parent) => posts.filter((p) => p.authorId === parent.id),
postCount: (parent) => posts.filter((p) => p.authorId === parent.id).length,
},
Post: {
author: (parent) => users.find((u) => u.id === parent.authorId),
comments: (parent) => comments.filter((c) => c.postId === parent.id),
commentCount: (parent) =>
comments.filter((c) => c.postId === parent.id).length,
},
Comment: {
author: (parent) => users.find((u) => u.id === parent.authorId),
post: (parent) => posts.find((p) => p.id === parent.postId),
},
Mutation: {
createUser: (_, { name, email }) => {
const newUser = { id: String(users.length + 1), name, email };
users.push(newUser);
return newUser;
},
createPost: (_, { title, body, authorId }) => {
const newPost = { id: String(posts.length + 1), title, body, authorId };
posts.push(newPost);
return newPost;
},
addComment: (_, { text, postId, authorId }) => {
const newComment = {
id: String(comments.length + 1),
text,
postId,
authorId,
};
comments.push(newComment);
return newComment;
},
},
};
// ─── Start Server ────────────────────────────────────────────────────────────
const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
});
console.log(`🚀 GraphQL Server ready at: ${url}`);
// Open: http://localhost:4000 → Apollo Sandbox (interactive playground!)Example 3: GraphQL Client — Fetch Queries (Vanilla JS)
// ✅ GraphQL Client: single endpoint, pick exactly what you need
const GRAPHQL_URL = "http://localhost:4000/";
// ─── Generic GraphQL fetch helper ────────────────────────────────────────────
async function gql(query, variables = {}) {
const response = await fetch(GRAPHQL_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query, variables }),
});
const { data, errors } = await response.json();
if (errors) throw new Error(errors[0].message);
return data;
}
// ─── Example 1: Get only user names (no wasted data) ─────────────────────────
async function getUserNames() {
const data = await gql(`
query {
users {
name # ← Only name! Server won't even compute email
}
}
`);
console.log("User names:", data.users);
// [{ name: 'Alice Johnson' }, { name: 'Bob Smith' }]
}
// ─── Example 2: Get post with author + comments in ONE request ────────────────
async function getPostDetails(postId) {
const data = await gql(
`
query GetPost($id: ID!) {
post(id: $id) {
title
body
author {
name
email
}
comments {
text
author {
name
}
}
commentCount
}
}
`,
{ id: postId }
);
console.log("Post details:", data.post);
// {
// title: 'GraphQL is Amazing',
// body: 'Here is why...',
// author: { name: 'Alice Johnson', email: 'alice@example.com' },
// comments: [{ text: 'Great post!', author: { name: 'Bob Smith' } }],
// commentCount: 2
// }
// 🎉 All in ONE HTTP request!
}
// ─── Example 3: Mutation — Create a new user ─────────────────────────────────
async function createUser(name, email) {
const data = await gql(
`
mutation CreateUser($name: String!, $email: String!) {
createUser(name: $name, email: $email) {
id
name
}
}
`,
{ name, email }
);
console.log("Created user:", data.createUser);
// { id: '3', name: 'Charlie' }
}
// ─── Example 4: Dashboard query — multiple resources at once ─────────────────
async function getDashboard() {
// One request to get ALL dashboard data
const data = await gql(`
query Dashboard {
users {
name
postCount # ← computed field
}
posts {
title
commentCount
author {
name
}
}
}
`);
console.log("Dashboard:", data);
// users and posts fetched together!
}
// Run examples
await getUserNames();
await getPostDetails("1");
await createUser("Charlie", "charlie@example.com");
await getDashboard();Example 4: GraphQL Subscriptions — Real-Time Updates
// ✅ GraphQL Subscriptions with WebSockets (using graphql-ws)
// Server side addition:
import { createServer } from "http";
import { WebSocketServer } from "ws";
import { useServer } from "graphql-ws/lib/use/ws";
import { makeExecutableSchema } from "@graphql-tools/schema";
import { PubSub } from "graphql-subscriptions";
const pubsub = new PubSub();
const typeDefs = `#graphql
# ... (previous types) ...
type Subscription {
newPost: Post!
newComment(postId: ID!): Comment!
}
`;
const resolvers = {
// ... (previous resolvers) ...
Subscription: {
newPost: {
subscribe: () => pubsub.asyncIterator(["NEW_POST"]),
},
newComment: {
subscribe: (_, { postId }) =>
pubsub.asyncIterator([`NEW_COMMENT_${postId}`]),
},
},
Mutation: {
createPost: (_, args) => {
const newPost = { ...args, id: String(posts.length + 1) };
posts.push(newPost);
// Publish to all subscribers!
pubsub.publish("NEW_POST", { newPost });
return newPost;
},
},
};
// Client side — listen for real-time updates:
// (Using graphql-ws client library)
import { createClient } from "graphql-ws";
const wsClient = createClient({ url: "ws://localhost:4000/graphql" });
// Subscribe to new posts in real time
wsClient.subscribe(
{
query: `
subscription {
newPost {
title
author { name }
}
}
`,
},
{
next: ({ data }) => {
console.log("📬 New post received:", data.newPost);
// { title: 'New Post!', author: { name: 'Alice' } }
},
error: (err) => console.error(err),
complete: () => console.log("Subscription ended"),
}
);🧭 When to Use What
Decision Tree: REST vs GraphQL
════════════════════════════════════════════════════════════
START
│
▼
Is this a public API consumed by external developers?
│
├── YES ──► Simple, well-documented resources?
│ │
│ ├── YES ──► ✅ USE REST (familiar, cacheable, simple)
│ │
│ └── NO ───► 🤔 Consider GraphQL (flexibility for consumers)
│
└── NO ───► Multiple client types? (mobile, web, desktop, IoT)
│
├── YES ──► ✅ USE GRAPHQL (each client gets exactly what it needs)
│
└── NO ───► Complex, interconnected data (social graph, e-commerce)?
│
├── YES ──► ✅ USE GRAPHQL
│
└── NO ───► Simple CRUD operations?
│
└── ✅ USE REST (simpler, better HTTP caching)Use GraphQL When:
| Scenario | Why GraphQL? |
|---|---|
| Mobile + Web apps with same backend | Different data needs per client |
| Complex nested/relational data | Avoid N+1 requests |
| Rapid product iteration | Add fields without breaking clients |
| Real-time features | Native subscription support |
| Multiple teams on same API | Strong typed contracts |
| Data aggregation from multiple sources | Single endpoint, schema stitching |
Use REST When:
| Scenario | Why REST? |
|---|---|
| Simple CRUD API | Simpler to reason about |
| Public/third-party APIs | Familiar to all developers |
| File uploads/downloads | Better HTTP support |
| Heavy HTTP caching needed | GET requests cache natively |
| Microservices with clear boundaries | One resource = one endpoint |
| Team unfamiliar with GraphQL | Lower learning curve |
⚡ N+1 Problem & DataLoader Solution
// ❌ The N+1 Problem in GraphQL
// Querying 10 posts → each triggers 1 DB call for author → 10 extra calls!
// Resolver without optimization:
Post: {
author: (parent) => db.findUser(parent.authorId); // ← Called N times!
}
// ✅ Solution: DataLoader (batching + caching)
import DataLoader from "dataloader";
// Create a loader that batches all user IDs into ONE DB query
const userLoader = new DataLoader(async (userIds) => {
console.log(`Fetching users: ${userIds}`); // Only called ONCE!
const users = await db.findUsers({ id: { $in: userIds } });
// Must return in same order as input IDs
return userIds.map((id) => users.find((u) => u.id === id));
});
// Updated resolver — now batched automatically
Post: {
author: (parent) => userLoader.load(parent.authorId);
// ↑ Batched! 10 posts = 1 DB call ✅
}🔍 Quick Cheat Sheet
GraphQL Operation Types
══════════════════════════════════════════════════════
query → Read data (like GET in REST)
mutation → Write data (like POST/PUT/DELETE in REST)
subscription → Real-time data (like WebSockets in REST)
GraphQL Response Structure
══════════════════════════════════════════════════════
{
"data": { // ← Always present on success
"user": { ... }
},
"errors": [ // ← Present on error (alongside data!)
{ "message": "...", "locations": [...], "path": [...] }
]
}
GraphQL Scalar Types
══════════════════════════════════════════════════════
Int → 32-bit integer
Float → Double-precision float
String → UTF-8 string
Boolean → true / false
ID → Unique identifier (serialized as String)
Modifiers
══════════════════════════════════════════════════════
String → nullable string (can be null)
String! → non-null string (required)
[String] → nullable list of nullable strings
[String!]! → non-null list of non-null strings🛠️ Ecosystem & Tools
| Tool | Purpose |
|---|---|
| Apollo Server | GraphQL server for Node.js |
| Apollo Client | GraphQL client for React/JS |
| GraphiQL | In-browser IDE for GraphQL |
| Apollo Studio | Schema management, monitoring |
| Hasura | Instant GraphQL on Postgres |
| Prisma | Type-safe DB client + GraphQL integration |
| DataLoader | Batching & caching for resolvers |
| graphql-ws | WebSocket protocol for Subscriptions |
| Pothos | Code-first GraphQL schema builder |
| Nexus | Type-safe schema-first approach |
🏁 Summary
REST GraphQL
════════════════════════════════════════════════════════════════
"I'll give you what I think "Tell me exactly what you want,
you need in a fixed format" and I'll give you precisely that"
Multiple endpoints Single endpoint (/graphql)
Server-driven shape Client-driven shape
Over/under-fetch common Exact data, every time
HTTP verbs (GET/POST/PUT) Operations (query/mutation/subscription)
URL-based versioning Schema evolution
HTTP caching built-in Custom caching needed
Simple, familiar Powerful, flexible
Both are valid. Choose based on your use case! 🎯💡 Pro Tip: Many modern apps use both — REST for file uploads, simple endpoints, and public APIs; GraphQL for complex data-fetching in their frontend apps.
