🔷 GraphQL

Modern Query Language for APIs

What is GraphQL?

GraphQL is a query language for APIs and a runtime for executing those queries. It provides a complete and understandable description of the data in your API, giving clients the power to ask for exactly what they need.

🆚 GraphQL vs REST

// REST - Multiple endpoints, over-fetching
GET /api/users/123
// Returns: { id, name, email, bio, avatar, posts, ... }

GET /api/users/123/posts
// Returns: [{ id, title, content, author, comments, ... }]

// GraphQL - Single endpoint, exact data
POST /graphql
{
  query {
    user(id: 123) {
      name
      email
      posts {
        title
      }
    }
  }
}
// Returns only: { user: { name, email, posts: [{ title }] } }

// GraphQL Advantages:
// ✅ Request exactly what you need (no over/under-fetching)
// ✅ Single request for nested resources
// ✅ Strongly typed schema
// ✅ Self-documenting
// ✅ Faster iteration (no endpoint versioning)
// ✅ Real-time with subscriptions

// REST Advantages:
// ✅ Simpler for simple APIs
// ✅ Better HTTP caching
// ✅ More familiar
// ✅ File upload easier

📝 GraphQL Basics

Schema and Types

// Schema defines API structure
type User {
  id: ID!           # ! means required
  name: String!
  email: String!
  age: Int
  posts: [Post!]!   # Array of Posts (never null)
  createdAt: String
}

type Post {
  id: ID!
  title: String!
  content: String!
  author: User!
  comments: [Comment!]!
  published: Boolean!
}

type Comment {
  id: ID!
  text: String!
  author: User!
  post: Post!
}

# Query type - read operations
type Query {
  user(id: ID!): User
  users: [User!]!
  post(id: ID!): Post
  posts(limit: Int): [Post!]!
}

# Mutation type - write operations
type Mutation {
  createUser(name: String!, email: String!): User!
  updateUser(id: ID!, name: String, email: String): User!
  deleteUser(id: ID!): Boolean!
  createPost(title: String!, content: String!): Post!
}

# Subscription type - real-time updates
type Subscription {
  postAdded: Post!
  userUpdated(id: ID!): User!
}

# Built-in scalar types:
# - Int: 32-bit integer
# - Float: floating-point number
# - String: UTF-8 string
# - Boolean: true or false
# - ID: unique identifier

Queries

// Basic query
{
  users {
    id
    name
    email
  }
}

// Query with arguments
{
  user(id: "123") {
    name
    email
  }
}

// Nested query
{
  user(id: "123") {
    name
    posts {
      title
      comments {
        text
        author {
          name
        }
      }
    }
  }
}

// Multiple queries
{
  user1: user(id: "123") {
    name
  }
  user2: user(id: "456") {
    name
  }
}

// Query with variables
query GetUser($id: ID!) {
  user(id: $id) {
    name
    email
  }
}
// Variables: { "id": "123" }

// Default values
query GetPosts($limit: Int = 10) {
  posts(limit: $limit) {
    title
  }
}

// Fragments (reusable fields)
fragment UserInfo on User {
  id
  name
  email
}

{
  user1: user(id: "123") {
    ...UserInfo
  }
  user2: user(id: "456") {
    ...UserInfo
  }
}

// Inline fragments (type conditions)
{
  search(text: "hello") {
    ... on User {
      name
      email
    }
    ... on Post {
      title
      content
    }
  }
}

Mutations

// Create user
mutation {
  createUser(name: "John", email: "john@example.com") {
    id
    name
    email
  }
}

// Update user
mutation {
  updateUser(id: "123", name: "John Doe") {
    id
    name
    email
  }
}

// Delete user
mutation {
  deleteUser(id: "123")
}

// Mutation with variables
mutation CreateUser($name: String!, $email: String!) {
  createUser(name: $name, email: $email) {
    id
    name
    email
    createdAt
  }
}
// Variables: { "name": "John", "email": "john@example.com" }

// Multiple mutations (executed sequentially)
mutation {
  user1: createUser(name: "John", email: "john@example.com") {
    id
  }
  user2: createUser(name: "Jane", email: "jane@example.com") {
    id
  }
}

// Return related data after mutation
mutation {
  createPost(title: "My Post", content: "Content here") {
    id
    title
    author {
      name
      email
    }
    comments {
      id
      text
    }
  }
}

Subscriptions

// Subscribe to new posts
subscription {
  postAdded {
    id
    title
    author {
      name
    }
  }
}

// Subscribe with variables
subscription OnUserUpdated($userId: ID!) {
  userUpdated(id: $userId) {
    id
    name
    email
  }
}

// WebSocket connection required for subscriptions
// Server pushes updates to client in real-time

🌐 Making GraphQL Requests

Using Fetch

// Basic query
async function fetchUser(id) {
    let response = await fetch('https://api.example.com/graphql', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({
            query: `
                query {
                    user(id: "${id}") {
                        id
                        name
                        email
                    }
                }
            `
        })
    });
    
    let { data, errors } = await response.json();
    
    if (errors) {
        console.error('GraphQL errors:', errors);
        throw new Error(errors[0].message);
    }
    
    return data.user;
}

// With variables
async function fetchUser(id) {
    let response = await fetch('https://api.example.com/graphql', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({
            query: `
                query GetUser($id: ID!) {
                    user(id: $id) {
                        id
                        name
                        email
                    }
                }
            `,
            variables: {
                id: id
            }
        })
    });
    
    let { data, errors } = await response.json();
    
    if (errors) {
        throw new Error(errors[0].message);
    }
    
    return data.user;
}

// Mutation
async function createUser(name, email) {
    let response = await fetch('https://api.example.com/graphql', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${token}`
        },
        body: JSON.stringify({
            query: `
                mutation CreateUser($name: String!, $email: String!) {
                    createUser(name: $name, email: $email) {
                        id
                        name
                        email
                    }
                }
            `,
            variables: {
                name,
                email
            }
        })
    });
    
    let { data, errors } = await response.json();
    
    if (errors) {
        throw new Error(errors[0].message);
    }
    
    return data.createUser;
}

GraphQL Client Class

class GraphQLClient {
    constructor(endpoint, options = {}) {
        this.endpoint = endpoint;
        this.headers = options.headers || {};
    }
    
    async query(query, variables = {}) {
        let response = await fetch(this.endpoint, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                ...this.headers
            },
            body: JSON.stringify({
                query,
                variables
            })
        });
        
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}`);
        }
        
        let result = await response.json();
        
        if (result.errors) {
            throw new GraphQLError(result.errors);
        }
        
        return result.data;
    }
    
    async mutate(mutation, variables = {}) {
        return this.query(mutation, variables);
    }
    
    setHeader(key, value) {
        this.headers[key] = value;
    }
    
    setAuth(token) {
        this.setHeader('Authorization', `Bearer ${token}`);
    }
}

class GraphQLError extends Error {
    constructor(errors) {
        super(errors[0].message);
        this.name = 'GraphQLError';
        this.errors = errors;
    }
}

// Usage
let client = new GraphQLClient('https://api.example.com/graphql');
client.setAuth('your-token');

// Query
let user = await client.query(`
    query GetUser($id: ID!) {
        user(id: $id) {
            id
            name
            email
        }
    }
`, { id: '123' });

// Mutation
let newUser = await client.mutate(`
    mutation CreateUser($name: String!, $email: String!) {
        createUser(name: $name, email: $email) {
            id
            name
            email
        }
    }
`, { name: 'John', email: 'john@example.com' });

📚 Apollo Client

// Install: npm install @apollo/client graphql

import { ApolloClient, InMemoryCache, gql } from '@apollo/client';

// Create client
let client = new ApolloClient({
    uri: 'https://api.example.com/graphql',
    cache: new InMemoryCache(),
    headers: {
        authorization: `Bearer ${token}`
    }
});

// Query with gql tag
let GET_USER = gql`
    query GetUser($id: ID!) {
        user(id: $id) {
            id
            name
            email
            posts {
                id
                title
            }
        }
    }
`;

let { data } = await client.query({
    query: GET_USER,
    variables: { id: '123' }
});

console.log(data.user);

// Mutation
let CREATE_USER = gql`
    mutation CreateUser($name: String!, $email: String!) {
        createUser(name: $name, email: $email) {
            id
            name
            email
        }
    }
`;

let { data } = await client.mutate({
    mutation: CREATE_USER,
    variables: {
        name: 'John',
        email: 'john@example.com'
    }
});

// Subscription
import { split, HttpLink, WebSocketLink } from '@apollo/client/core';
import { getMainDefinition } from '@apollo/client/utilities';

let httpLink = new HttpLink({
    uri: 'https://api.example.com/graphql'
});

let wsLink = new WebSocketLink({
    uri: 'ws://api.example.com/graphql',
    options: {
        reconnect: true
    }
});

// Split based on operation type
let link = split(
    ({ query }) => {
        let definition = getMainDefinition(query);
        return (
            definition.kind === 'OperationDefinition' &&
            definition.operation === 'subscription'
        );
    },
    wsLink,
    httpLink
);

let client = new ApolloClient({
    link,
    cache: new InMemoryCache()
});

// Subscribe
let POSTS_SUBSCRIPTION = gql`
    subscription {
        postAdded {
            id
            title
            author {
                name
            }
        }
    }
`;

let subscription = client.subscribe({
    query: POSTS_SUBSCRIPTION
}).subscribe({
    next: ({ data }) => {
        console.log('New post:', data.postAdded);
    },
    error: error => {
        console.error('Subscription error:', error);
    }
});

💡 Practical Examples

Blog Application

class BlogAPI {
    constructor(client) {
        this.client = client;
    }
    
    async getPosts(limit = 10) {
        return this.client.query(`
            query GetPosts($limit: Int!) {
                posts(limit: $limit) {
                    id
                    title
                    content
                    author {
                        id
                        name
                    }
                    comments {
                        id
                        text
                        author {
                            name
                        }
                    }
                    createdAt
                }
            }
        `, { limit });
    }
    
    async getPost(id) {
        return this.client.query(`
            query GetPost($id: ID!) {
                post(id: $id) {
                    id
                    title
                    content
                    author {
                        id
                        name
                        email
                    }
                    comments {
                        id
                        text
                        author {
                            name
                        }
                        createdAt
                    }
                }
            }
        `, { id });
    }
    
    async createPost(title, content) {
        return this.client.mutate(`
            mutation CreatePost($title: String!, $content: String!) {
                createPost(title: $title, content: $content) {
                    id
                    title
                    content
                    author {
                        name
                    }
                    createdAt
                }
            }
        `, { title, content });
    }
    
    async addComment(postId, text) {
        return this.client.mutate(`
            mutation AddComment($postId: ID!, $text: String!) {
                addComment(postId: $postId, text: $text) {
                    id
                    text
                    author {
                        name
                    }
                    createdAt
                }
            }
        `, { postId, text });
    }
    
    async deletePost(id) {
        return this.client.mutate(`
            mutation DeletePost($id: ID!) {
                deletePost(id: $id)
            }
        `, { id });
    }
}

// Usage
let client = new GraphQLClient('https://api.example.com/graphql');
let blog = new BlogAPI(client);

let posts = await blog.getPosts(10);
let post = await blog.getPost('123');
let newPost = await blog.createPost('My Post', 'Content here');
await blog.addComment('123', 'Great post!');

Pagination

// Offset-based pagination
{
  posts(offset: 0, limit: 20) {
    id
    title
  }
}

// Cursor-based pagination (Relay-style)
{
  posts(first: 20, after: "cursor") {
    edges {
      node {
        id
        title
      }
      cursor
    }
    pageInfo {
      hasNextPage
      hasPreviousPage
      startCursor
      endCursor
    }
  }
}

// Fetch all pages
async function* fetchAllPosts(client) {
    let cursor = null;
    let hasNextPage = true;
    
    while (hasNextPage) {
        let result = await client.query(`
            query GetPosts($cursor: String) {
                posts(first: 20, after: $cursor) {
                    edges {
                        node {
                            id
                            title
                        }
                        cursor
                    }
                    pageInfo {
                        hasNextPage
                        endCursor
                    }
                }
            }
        `, { cursor });
        
        yield result.posts.edges.map(edge => edge.node);
        
        hasNextPage = result.posts.pageInfo.hasNextPage;
        cursor = result.posts.pageInfo.endCursor;
    }
}

// Usage
for await (let posts of fetchAllPosts(client)) {
    console.log('Page:', posts);
}

Caching Strategy

class CachedGraphQLClient extends GraphQLClient {
    constructor(endpoint, options = {}) {
        super(endpoint, options);
        this.cache = new Map();
        this.ttl = options.ttl || 60000;  // 1 minute
    }
    
    getCacheKey(query, variables) {
        return JSON.stringify({ query, variables });
    }
    
    async query(query, variables = {}) {
        let key = this.getCacheKey(query, variables);
        
        if (this.cache.has(key)) {
            let { data, timestamp } = this.cache.get(key);
            if (Date.now() - timestamp < this.ttl) {
                return data;
            }
        }
        
        let data = await super.query(query, variables);
        
        this.cache.set(key, {
            data,
            timestamp: Date.now()
        });
        
        return data;
    }
    
    invalidate(pattern) {
        if (pattern) {
            for (let key of this.cache.keys()) {
                if (key.includes(pattern)) {
                    this.cache.delete(key);
                }
            }
        } else {
            this.cache.clear();
        }
    }
}

// Usage
let client = new CachedGraphQLClient('https://api.example.com/graphql', {
    ttl: 30000  // 30 seconds
});

let user = await client.query('...', { id: '123' });  // From server
let userAgain = await client.query('...', { id: '123' });  // From cache

client.invalidate('user');  // Clear user-related cache

Error Handling

async function safeQuery(client, query, variables) {
    try {
        return await client.query(query, variables);
    } catch (error) {
        if (error instanceof GraphQLError) {
            // Handle specific GraphQL errors
            error.errors.forEach(err => {
                if (err.extensions?.code === 'UNAUTHENTICATED') {
                    console.log('Not authenticated, redirecting...');
                    redirectToLogin();
                } else if (err.extensions?.code === 'FORBIDDEN') {
                    console.log('Access denied');
                } else if (err.extensions?.code === 'NOT_FOUND') {
                    console.log('Resource not found');
                } else {
                    console.error('GraphQL error:', err.message);
                }
            });
        } else {
            // Network or other errors
            console.error('Request failed:', error);
        }
        throw error;
    }
}

// Usage
try {
    let user = await safeQuery(client, GET_USER, { id: '123' });
} catch (error) {
    // Error already logged
}

⚡ Advanced Features

// Directives
// @include - conditionally include field
query GetUser($id: ID!, $withPosts: Boolean!) {
  user(id: $id) {
    name
    email
    posts @include(if: $withPosts) {
      title
    }
  }
}

// @skip - conditionally skip field
query GetUser($id: ID!, $skipEmail: Boolean!) {
  user(id: $id) {
    name
    email @skip(if: $skipEmail)
  }
}

// Aliases - rename fields in result
{
  admin: user(id: "1") {
    name
  }
  moderator: user(id: "2") {
    name
  }
}

// Interfaces - abstract type
interface Node {
  id: ID!
}

type User implements Node {
  id: ID!
  name: String!
}

type Post implements Node {
  id: ID!
  title: String!
}

// Unions - one of several types
union SearchResult = User | Post | Comment

{
  search(text: "hello") {
    ... on User {
      name
    }
    ... on Post {
      title
    }
    ... on Comment {
      text
    }
  }
}

// Input types - complex arguments
input CreateUserInput {
  name: String!
  email: String!
  age: Int
}

mutation {
  createUser(input: {
    name: "John"
    email: "john@example.com"
    age: 30
  }) {
    id
    name
  }
}

🎯 Key Takeaways