Skip to content

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

FeatureREST APIGraphQL
EndpointsMultiple (/users, /posts, /comments)Single (/graphql)
Data fetchingFixed response shapeClient defines shape
Over-fetching✅ Common — get unused fields❌ Eliminated
Under-fetching✅ Common — need multiple requests❌ Eliminated
Versioning/v1/users, /v2/usersSchema evolution (no versioning)
Type systemOptional (OpenAPI/Swagger)Built-in, mandatory
Real-timePolling / WebSockets (manual)Native Subscriptions
CachingHTTP caching (GET requests)Requires custom caching (e.g. Apollo)
Learning curveLowMedium
ToolingMatureGrowing (GraphiQL, Apollo, etc.)
Best forSimple CRUD, public APIsComplex data, multiple clients

🗺️ Architecture Diagram

text
                        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.

graphql
# 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)

graphql
# 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)

graphql
mutation CreateUser {
  createUser(name: "Alice", email: "alice@example.com") {
    id
    name
  }
}

4. Subscriptions (Real-time)

graphql
subscription OnNewPost {
  newPost {
    title
    author {
      name
    }
  }
}

💻 JavaScript Code Examples

Example 1: REST API — Traditional Approach

javascript
// ❌ 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)

javascript
// ✅ 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)

javascript
// ✅ 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

javascript
// ✅ 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

text
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:

ScenarioWhy GraphQL?
Mobile + Web apps with same backendDifferent data needs per client
Complex nested/relational dataAvoid N+1 requests
Rapid product iterationAdd fields without breaking clients
Real-time featuresNative subscription support
Multiple teams on same APIStrong typed contracts
Data aggregation from multiple sourcesSingle endpoint, schema stitching

Use REST When:

ScenarioWhy REST?
Simple CRUD APISimpler to reason about
Public/third-party APIsFamiliar to all developers
File uploads/downloadsBetter HTTP support
Heavy HTTP caching neededGET requests cache natively
Microservices with clear boundariesOne resource = one endpoint
Team unfamiliar with GraphQLLower learning curve

⚡ N+1 Problem & DataLoader Solution

javascript
// ❌ 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

text
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

ToolPurpose
Apollo ServerGraphQL server for Node.js
Apollo ClientGraphQL client for React/JS
GraphiQLIn-browser IDE for GraphQL
Apollo StudioSchema management, monitoring
HasuraInstant GraphQL on Postgres
PrismaType-safe DB client + GraphQL integration
DataLoaderBatching & caching for resolvers
graphql-wsWebSocket protocol for Subscriptions
PothosCode-first GraphQL schema builder
NexusType-safe schema-first approach

🏁 Summary

text
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.

Released under the ISC License.